diff --git a/app/src/main/java/foundation/e/apps/data/application/data/Application.kt b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt index 0d8bd51844ed68b10a43f3734d140cdf02836456..fb4646ebeeac51be74a3e9f48021d3ee3dc6c6ae 100644 --- a/app/src/main/java/foundation/e/apps/data/application/data/Application.kt +++ b/app/src/main/java/foundation/e/apps/data/application/data/Application.kt @@ -105,6 +105,7 @@ data class Application( var contentRating: ContentRating = ContentRating(), @SerializedName(value = "antifeatures") val antiFeatures: List> = emptyList(), + var isSystemApp: Boolean = false, ) { fun updateType() { this.type = if (this.is_pwa) PWA else NATIVE 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 new file mode 100644 index 0000000000000000000000000000000000000000..b5f7d48b46434f420793264bada8983814335f88 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/gitlab/SystemAppDefinitionApi.kt @@ -0,0 +1,38 @@ +/* + * 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 new file mode 100644 index 0000000000000000000000000000000000000000..04812c24ad43ebed3552b95e8f8eba144c07aca1 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/gitlab/SystemAppsUpdatesRepository.kt @@ -0,0 +1,151 @@ +/* + * 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 android.content.Context +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.SystemAppInfo +import foundation.e.apps.data.gitlab.models.SystemAppProject +import foundation.e.apps.data.gitlab.models.toApplication +import foundation.e.apps.data.handleNetworkResult +import foundation.e.apps.install.pkg.AppLoungePackageManager +import foundation.e.apps.utils.SystemInfoProvider +import javax.inject.Inject +import javax.inject.Singleton +import timber.log.Timber + +@Singleton +class SystemAppsUpdatesRepository @Inject constructor( + @ApplicationContext private val context: Context, + private val updatableSystemAppsApi: UpdatableSystemAppsApi, + private val systemAppDefinitionApi: SystemAppDefinitionApi, + private val applicationDataManager: ApplicationDataManager, + private val appLoungePackageManager: AppLoungePackageManager, +) { + + private var systemAppProjectList = mutableListOf() + + suspend fun fetchUpdatableSystemApps() { + val result = handleNetworkResult { + val response = updatableSystemAppsApi.getUpdatableSystemApps() + if (response.isSuccessful && !response.body().isNullOrEmpty()) { + response.body()?.let { systemAppProjectList.addAll(it) } + } else { + Timber.e("Failed to fetch updatable apps: ${response.errorBody()?.string()}") + } + } + + if (!result.isSuccess()) { + Timber.e("Network error when fetching updatable apps - ${result.message}") + } + } + + fun getUpdatableSystemApps(): List { + return systemAppProjectList.map { it.packageName } + } + + private fun isSystemAppBlocked( + systemAppInfo: SystemAppInfo, + sdkLevel: Int, + device: String, + ): Boolean { + return systemAppInfo.run { + sdkLevel < minSdk || + blockedAndroid?.contains(sdkLevel) == true || + blockedDevices?.contains(device) == true || + blockedDevices?.contains("${device}@${sdkLevel}") == true + } + } + + private suspend fun getSystemAppUpdateInfo( + packageName: String, + releaseType: String, + sdkLevel: Int, + device: String, + ): Application? { + + val projectId = + systemAppProjectList.find { it.packageName == packageName }?.projectId ?: return null + + val response = systemAppDefinitionApi.getSystemAppUpdateInfo(projectId, releaseType) + val systemAppInfo = response.body() + + return if (systemAppInfo == null) { + Timber.e("Null app info for: $packageName, response: ${response.errorBody()?.string()}") + null + } else if (isSystemAppBlocked(systemAppInfo, sdkLevel, device)) { + Timber.e("Blocked system app: $packageName, details: $systemAppInfo") + null + } else { + systemAppInfo.toApplication() + } + } + + private fun getSdkLevel(): Int { + return Build.VERSION.SDK_INT + } + + private fun getDevice(): String { + return SystemInfoProvider.getSystemProperty(SystemInfoProvider.KEY_LINEAGE_DEVICE) ?: "" + } + + private fun getSystemReleaseType(): String { + return SystemInfoProvider.getSystemProperty(SystemInfoProvider.KEY_LINEAGE_RELEASE_TYPE) ?: "" + } + + suspend fun getSystemUpdates(): List { + val updateList = mutableListOf() + val releaseType = getSystemReleaseType() + val sdkLevel = getSdkLevel() + val device = getDevice() + + val updatableApps = getUpdatableSystemApps() + updatableApps.forEach { + + if (!appLoungePackageManager.isInstalled(it)) { + // Don't install for system apps which are removed (by root or otherwise) + return@forEach + } + + val result = handleNetworkResult { + getSystemAppUpdateInfo( + it, + releaseType, + sdkLevel, + device, + ) + } + + result.data?.run { + applicationDataManager.updateStatus(this) + updateList.add(this) + updateSource(context) + } + + if (!result.isSuccess()) { + Timber.e("Failed to get system app info for $it - ${result.message}") + } + } + + return updateList + } + +} diff --git a/app/src/main/java/foundation/e/apps/data/gitlab/UpdatableSystemAppsApi.kt b/app/src/main/java/foundation/e/apps/data/gitlab/UpdatableSystemAppsApi.kt new file mode 100644 index 0000000000000000000000000000000000000000..2ea9774faaa5c3cbee98a089fec57a5fe56af16b --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/gitlab/UpdatableSystemAppsApi.kt @@ -0,0 +1,34 @@ +/* + * 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.SystemAppProject +import retrofit2.Response +import retrofit2.http.GET + +interface UpdatableSystemAppsApi { + + companion object { + const val BASE_URL = + "https://gitlab.e.foundation/e/os/system-apps-update-info/-/raw/main/" + } + + @GET("updatable_system_apps.json?inline=false") + suspend fun getUpdatableSystemApps(): Response> + +} diff --git a/app/src/main/java/foundation/e/apps/data/gitlab/models/SystemAppInfo.kt b/app/src/main/java/foundation/e/apps/data/gitlab/models/SystemAppInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..5ccb2239a82a5e49d018b922942de4f3e8764c1d --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/gitlab/models/SystemAppInfo.kt @@ -0,0 +1,59 @@ +/* + * 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.models + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.squareup.moshi.Json +import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.enums.FilterLevel +import foundation.e.apps.data.enums.Origin +import java.util.UUID + +@JsonIgnoreProperties(ignoreUnknown = true) +data class SystemAppInfo( + val name: String, + @Json(name = "package_name") val packageName: String, + @Json(name = "version_code") val versionCode: Int, + @Json(name = "min_sdk") val minSdk: Int, + @Json(name = "version_name") val versionName: String, + @Json(name = "url") val downloadUrl: String, + @Json(name = "size") val size: Long?, + @Json(name = "author_name") val authorName: String?, + val priority: Boolean?, + @Json(name = "blocked_android") val blockedAndroid: List?, + @Json(name = "blocked_devices") val blockedDevices: List?, +) + +private const val RANDOM_SIZE = 1L + +fun SystemAppInfo.toApplication(): Application { + return Application( + _id = UUID.randomUUID().toString(), + author = authorName ?: "eFoundation", + description = "", + latest_version_code = versionCode, + latest_version_number = versionName, + name = name, + package_name = packageName, + origin = Origin.GITLAB_RELEASES, + originalSize = size ?: RANDOM_SIZE, + url = downloadUrl, + isSystemApp = true, + filterLevel = FilterLevel.NONE, + ) +} 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 new file mode 100644 index 0000000000000000000000000000000000000000..edc7f2bffdeaf584e4e54aaceae0412b5dd5ed9f --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/gitlab/models/SystemAppProject.kt @@ -0,0 +1,23 @@ +/* + * 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.models + +data class SystemAppProject( + val packageName: String, + val projectId: Int, +) 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 576302cc9e076d7567adbfd6dc3eb471ccd7e21f..56914f6165379009bf6340177fd04025904b5d25 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 @@ -29,6 +29,8 @@ import foundation.e.apps.data.cleanapk.CleanApkRetrofit 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.parentalcontrol.fdroid.FDroidMonitorApi import foundation.e.apps.data.parentalcontrol.googleplay.AgeGroupApi import foundation.e.apps.di.network.NetworkModule.getYamlFactory @@ -131,4 +133,32 @@ class RetrofitApiModule { .create(FDroidMonitorApi::class.java) } + @Singleton + @Provides + fun provideUpdatableSystemAppsApi( + okHttpClient: OkHttpClient, + moshi: Moshi, + ): UpdatableSystemAppsApi { + return Retrofit.Builder() + .baseUrl(UpdatableSystemAppsApi.BASE_URL) + .client(okHttpClient) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + .create(UpdatableSystemAppsApi::class.java) + } + + @Singleton + @Provides + fun provideSystemAppDefinitionApi( + okHttpClient: OkHttpClient, + moshi: Moshi, + ): SystemAppDefinitionApi { + return Retrofit.Builder() + .baseUrl(SystemAppDefinitionApi.BASE_URL) + .client(okHttpClient) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + .create(SystemAppDefinitionApi::class.java) + } + } diff --git a/app/src/main/java/foundation/e/apps/utils/SystemInfoProvider.kt b/app/src/main/java/foundation/e/apps/utils/SystemInfoProvider.kt index 68b72945dd783c447141506141e13068da1db55e..bf9fe2efe4f5ba4b0d23d6c5c5ec0e0afb89dcda 100644 --- a/app/src/main/java/foundation/e/apps/utils/SystemInfoProvider.kt +++ b/app/src/main/java/foundation/e/apps/utils/SystemInfoProvider.kt @@ -25,6 +25,8 @@ import org.json.JSONObject object SystemInfoProvider { const val KEY_LINEAGE_VERSION = "ro.lineage.version" + const val KEY_LINEAGE_RELEASE_TYPE = "ro.lineage.releasetype" + const val KEY_LINEAGE_DEVICE = "ro.lineage.device" @SuppressLint("PrivateApi") fun getSystemProperty(key: String?): String? {