diff --git a/app/src/main/java/foundation/e/apps/MainActivity.kt b/app/src/main/java/foundation/e/apps/MainActivity.kt index f4f0988ec2c093f2ff53ebf9ede1caad0aea552d..d6d5522d3c35c6f94e109c9f1556e83927a3d887 100644 --- a/app/src/main/java/foundation/e/apps/MainActivity.kt +++ b/app/src/main/java/foundation/e/apps/MainActivity.kt @@ -125,6 +125,7 @@ class MainActivity : AppCompatActivity() { viewModel.updateAppWarningList() viewModel.updateContentRatings() + viewModel.updateEligibleSystemAppsList() observeEvents() diff --git a/app/src/main/java/foundation/e/apps/data/application/ApplicationDataManager.kt b/app/src/main/java/foundation/e/apps/data/application/ApplicationDataManager.kt index b5daea5364d514d195c803bdd63283a40fdeaaec..15090a362b7c6ae9af4811534aa1be914c33d0ad 100644 --- a/app/src/main/java/foundation/e/apps/data/application/ApplicationDataManager.kt +++ b/app/src/main/java/foundation/e/apps/data/application/ApplicationDataManager.kt @@ -62,6 +62,7 @@ class ApplicationDataManager @Inject constructor( application.package_name.isBlank() -> FilterLevel.UNKNOWN !application.isFree && application.price.isBlank() -> FilterLevel.UI application.origin == Origin.CLEANAPK -> FilterLevel.NONE + application.origin == Origin.GITLAB -> FilterLevel.NONE !isRestricted(application) -> FilterLevel.NONE authData == null -> FilterLevel.UNKNOWN // cannot determine for gplay app !isApplicationVisible(application) -> FilterLevel.DATA 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 2ff78b9473eacf42ad59184d1062e3bd9c4622a4..6eadaa1ee03b17fdb915b960eacd928dc987633f 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,8 @@ 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 @@ -112,9 +114,12 @@ data class Application( fun updateSource(context: Context) { this.apply { - source = if (origin != Origin.CLEANAPK) "" - else if (is_pwa) context.getString(R.string.pwa) - else context.getString(R.string.open_source) + source = when { + origin == Origin.GITLAB -> context.getString(R.string.system_app) + origin == Origin.GPLAY -> "" + is_pwa -> context.getString(R.string.pwa) + else -> context.getString(R.string.open_source) + } } } } diff --git a/app/src/main/java/foundation/e/apps/data/application/downloadInfo/DownloadInfoApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/downloadInfo/DownloadInfoApiImpl.kt index 896510856ea5187641145002b685febf22a26717..5e64b98c63feba36a699677ef9f236bf651678f9 100644 --- a/app/src/main/java/foundation/e/apps/data/application/downloadInfo/DownloadInfoApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/application/downloadInfo/DownloadInfoApiImpl.kt @@ -68,6 +68,10 @@ class DownloadInfoApiImpl @Inject constructor( Origin.GPLAY -> { updateDownloadInfoFromGplay(appInstall, list) } + + Origin.GITLAB -> { + return // nothing to do as downloadURLList is already set + } } appInstall.downloadURLList = list diff --git a/app/src/main/java/foundation/e/apps/data/application/home/HomeApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/home/HomeApiImpl.kt index d8b91b1734f2fbd610a89a1e01b087ae39fa71b5..4f94e2afdc019563f3067f099e18ea46222d0e41 100644 --- a/app/src/main/java/foundation/e/apps/data/application/home/HomeApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/application/home/HomeApiImpl.kt @@ -120,6 +120,10 @@ class HomeApiImpl @Inject constructor( Source.PWA -> handleNetworkResult { handleCleanApkHomes(priorList, SearchApi.APP_TYPE_PWA) } + + Source.GITLAB -> { + ResultSupreme.Error(message = "Gitlab source not allowed") + } } setHomeErrorMessage(result.getResultStatus(), source) @@ -223,6 +227,7 @@ class HomeApiImpl @Inject constructor( if (apiStatus != ResultStatus.OK) { apiStatus.message = when (source) { Source.GPLAY -> ("GPlay home loading error\n" + apiStatus.message).trim() + Source.GITLAB -> ("Gitlab home not allowed\n" + apiStatus.message).trim() Source.OPEN -> ("Open Source home loading error\n" + apiStatus.message).trim() Source.PWA -> ("PWA home loading error\n" + apiStatus.message).trim() } diff --git a/app/src/main/java/foundation/e/apps/data/enums/Origin.kt b/app/src/main/java/foundation/e/apps/data/enums/Origin.kt index b8bd98c8338b754b8ea67e332ce26bc47e45ea28..eb2cdc782df98276e6c6455959dfc54458f273a6 100644 --- a/app/src/main/java/foundation/e/apps/data/enums/Origin.kt +++ b/app/src/main/java/foundation/e/apps/data/enums/Origin.kt @@ -20,5 +20,6 @@ package foundation.e.apps.data.enums enum class Origin { CLEANAPK, - GPLAY + GPLAY, + GITLAB, } diff --git a/app/src/main/java/foundation/e/apps/data/enums/Source.kt b/app/src/main/java/foundation/e/apps/data/enums/Source.kt index 86fd9c631eb93a798ce6b160b691f8b2f7418ef0..c672015c8a4c886020aed9a0b5ef7ab73466867b 100644 --- a/app/src/main/java/foundation/e/apps/data/enums/Source.kt +++ b/app/src/main/java/foundation/e/apps/data/enums/Source.kt @@ -19,6 +19,7 @@ package foundation.e.apps.data.enums enum class Source { GPLAY, + GITLAB, OPEN, PWA; @@ -27,6 +28,7 @@ enum class Source { return when (source) { "Open Source" -> OPEN "PWA" -> PWA + "GITLAB" -> GITLAB else -> GPLAY } } diff --git a/app/src/main/java/foundation/e/apps/data/gitlab/EligibleSystemAppsApi.kt b/app/src/main/java/foundation/e/apps/data/gitlab/EligibleSystemAppsApi.kt new file mode 100644 index 0000000000000000000000000000000000000000..86d59ca895d538071a2e43e228c4c156adf18c43 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/gitlab/EligibleSystemAppsApi.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2019-2023 E FOUNDATION + * + * 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 EligibleSystemAppsApi { + + 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 getAllEligibleApps(): Response> + +} \ No newline at end of file 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..1cfaa4d0e33ccb3fde20fb446641921b8fb1e53f --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/gitlab/SystemAppDefinitionApi.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2019-2023 E FOUNDATION + * + * 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 + +} \ No newline at end of file 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..b4ac6396479b14053528bdaf324e4b7c24fa7ad8 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/gitlab/SystemAppsUpdatesRepository.kt @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2019-2023 E FOUNDATION + * + * 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 eligibleSystemAppsApi: EligibleSystemAppsApi, + private val systemAppDefinitionApi: SystemAppDefinitionApi, + private val applicationDataManager: ApplicationDataManager, + private val appLoungePackageManager: AppLoungePackageManager, +) { + + private var systemAppProjectList = mutableListOf() + + suspend fun fetchAllEligibleApps() { + handleNetworkResult { + val response = eligibleSystemAppsApi.getAllEligibleApps() + if (response.isSuccessful && !response.body().isNullOrEmpty()) { + response.body()?.let { systemAppProjectList.addAll(it) } + } + } + } + + fun getAllEligibleApps(): List { + return systemAppProjectList.map { it.packageName } + } + + private fun isSystemAppBlacklisted( + systemAppInfo: SystemAppInfo, + sdkLevel: Int, + device: String, + ): Boolean { + return systemAppInfo.run { + sdkLevel < minSdk || + blacklistedAndroid?.contains(sdkLevel) == true || + blacklistedDevices?.contains(device) == true || + blacklistedDevices?.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 systemAppInfo = + systemAppDefinitionApi.getSystemAppUpdateInfo(projectId, releaseType).body() + + return if (systemAppInfo == null) { + Timber.e("Null app info for: $packageName") + null + } else if (isSystemAppBlacklisted(systemAppInfo, sdkLevel, device)) { + Timber.e("Blacklisted system app: $packageName, $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 eligibleApps = getAllEligibleApps() + eligibleApps.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 + } + +} \ No newline at end of file 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..e661b788abe693a9ebc824d7783919b29bf59a53 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/gitlab/models/SystemAppInfo.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2019-2023 E FOUNDATION + * + * 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, + val priority: Boolean?, + @Json(name = "blacklisted_android") val blacklistedAndroid: List?, + @Json(name = "blacklisted_devices") val blacklistedDevices: List?, +) + +fun SystemAppInfo.toApplication(): Application { + return Application( + _id = UUID.randomUUID().toString(), + author = "Murena SAS", + description = "", + latest_version_code = versionCode, + latest_version_number = versionName, + name = name, + package_name = packageName, + origin = Origin.GITLAB, + originalSize = 1, // so that the app is not filtered out, + url = downloadUrl, + isSystemApp = true, + filterLevel = FilterLevel.NONE, + ) +} \ No newline at end of file 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..b6c0a723d2f84a829c7499eaea84f3b08d0e4d31 --- /dev/null +++ b/app/src/main/java/foundation/e/apps/data/gitlab/models/SystemAppProject.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2019-2023 E FOUNDATION + * + * 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/data/updates/UpdatesManagerImpl.kt b/app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerImpl.kt index 7cc60ac310ec918f13b523df39f22d97e43146dc..3e075ef2d25aad49fa02f7178304a57495fe91d5 100644 --- a/app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/updates/UpdatesManagerImpl.kt @@ -34,6 +34,7 @@ import foundation.e.apps.data.playstore.PlayStoreRepositoryImpl import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.search.SearchApi.Companion.APP_TYPE_ANY import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.gitlab.SystemAppsUpdatesRepository import foundation.e.apps.data.handleNetworkResult import foundation.e.apps.data.preference.AppLoungePreference import foundation.e.apps.install.pkg.AppLoungePackageManager @@ -53,6 +54,7 @@ class UpdatesManagerImpl @Inject constructor( private val appLoungePreference: AppLoungePreference, private val fdroidRepository: FdroidRepository, private val blockedAppRepository: BlockedAppRepository, + private val systemAppsUpdatesRepository: SystemAppsUpdatesRepository, ) { companion object { @@ -123,8 +125,12 @@ class UpdatesManagerImpl @Inject constructor( status = if (status == ResultStatus.OK) status else gplayStatus } + val systemApps = getSystemUpdates() val nonFaultyUpdateList = faultyAppRepository.removeFaultyApps(updateList) - return Pair(nonFaultyUpdateList, status) + + arrangeWithSystemApps(updateList, nonFaultyUpdateList, systemApps) + + return Pair(updateList, status) } suspend fun getUpdatesOSS(): Pair, ResultStatus> { @@ -157,8 +163,30 @@ class UpdatesManagerImpl @Inject constructor( }, updateList) } + val systemApps = getSystemUpdates() val nonFaultyUpdateList = faultyAppRepository.removeFaultyApps(updateList) - return Pair(nonFaultyUpdateList, status) + + arrangeWithSystemApps(updateList, nonFaultyUpdateList, systemApps) + + return Pair(updateList, status) + } + + private suspend fun getSystemUpdates(): List { + val systemApps = mutableListOf() + getUpdatesFromApi({ + Pair(systemAppsUpdatesRepository.getSystemUpdates(), ResultStatus.OK) + }, systemApps) + return systemApps + } + + private fun arrangeWithSystemApps( + updateList: MutableList, + nonFaultyApps: List, + systemApps: List, + ) { + updateList.clear() + updateList.addAll(nonFaultyApps) + updateList.addAll(systemApps) } /** 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..d0e3ff85ca40432dcb3ae57d90a41bc146a66b2a 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.EligibleSystemAppsApi +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 provideEligibleSystemAppsApi( + okHttpClient: OkHttpClient, + moshi: Moshi, + ): EligibleSystemAppsApi { + return Retrofit.Builder() + .baseUrl(EligibleSystemAppsApi.BASE_URL) + .client(okHttpClient) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .build() + .create(EligibleSystemAppsApi::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/domain/ValidateAppAgeLimitUseCase.kt b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt index 994289f21415a5960a18b8b0b4fe77b45d1f1ba7..f852766c56dae789fc533c714d52cb3cef88330f 100644 --- a/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt +++ b/app/src/main/java/foundation/e/apps/domain/ValidateAppAgeLimitUseCase.kt @@ -56,6 +56,10 @@ class ValidateAppAgeLimitUseCase @Inject constructor( data = ContentRatingValidity(true) ) + isGitlabApp(app) -> ResultSupreme.Success( + data = ContentRatingValidity(true) + ) + isKnownNsfwApp(app) -> ResultSupreme.Success(data = ContentRatingValidity(false)) isCleanApkApp(app) -> ResultSupreme.Success( data = ContentRatingValidity(isValid = !isNsfwAppByCleanApkApi(app)) @@ -70,6 +74,10 @@ class ValidateAppAgeLimitUseCase @Inject constructor( } } + private fun isGitlabApp(app: AppInstall): Boolean { + return app.origin == Origin.GITLAB + } + private fun isCleanApkApp(app: AppInstall): Boolean { return app.id.isNotBlank() && app.origin == Origin.CLEANAPK diff --git a/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorkManager.kt b/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorkManager.kt index fa41365edc1f4379d44ce68aa7fb68a0db1c4803..9cac3976926777960bcd3cb45c8f72645c1418db 100644 --- a/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorkManager.kt +++ b/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorkManager.kt @@ -47,8 +47,11 @@ object UpdatesWorkManager { return OneTimeWorkRequest.Builder(UpdatesWorker::class.java).apply { setConstraints(buildWorkerConstraints()) addTag(USER_TAG) - }.setInputData(Data.Builder().putBoolean(UpdatesWorker.IS_AUTO_UPDATE, false).build()) - .build() + }.setInputData( + Data.Builder() + .putBoolean(UpdatesWorker.IS_AUTO_UPDATE, false) + .build() + ).build() } private fun buildWorkerConstraints() = Constraints.Builder().apply { diff --git a/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt b/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt index ee181a81a655bce1ace25a6cb5f385021d8879ca..88acbb49af10922fddefd69c850c410d969e4c33 100644 --- a/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt +++ b/app/src/main/java/foundation/e/apps/install/updates/UpdatesWorker.kt @@ -20,6 +20,7 @@ import foundation.e.apps.data.blockedApps.BlockedAppRepository import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.User import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.gitlab.SystemAppsUpdatesRepository import foundation.e.apps.data.login.AuthenticatorRepository import foundation.e.apps.data.preference.DataStoreManager import foundation.e.apps.data.updates.UpdatesManagerRepository @@ -40,6 +41,7 @@ class UpdatesWorker @AssistedInject constructor( private val authenticatorRepository: AuthenticatorRepository, private val appInstallProcessor: AppInstallProcessor, private val blockedAppRepository: BlockedAppRepository, + private val systemAppsUpdatesRepository: SystemAppsUpdatesRepository, ) : CoroutineWorker(context, params) { companion object { @@ -62,6 +64,7 @@ class UpdatesWorker @AssistedInject constructor( } refreshBlockedAppList() + refreshEligibleSystemApps() checkForUpdates() Result.success() } catch (e: Throwable) { @@ -80,6 +83,12 @@ class UpdatesWorker @AssistedInject constructor( } } + private suspend fun refreshEligibleSystemApps() { + if (systemAppsUpdatesRepository.getAllEligibleApps().isEmpty()) { + systemAppsUpdatesRepository.fetchAllEligibleApps() + } + } + private suspend fun checkManualUpdateRunning(): Boolean { val workInfos = withContext(Dispatchers.IO) { diff --git a/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt b/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt index 83d9d7e940f1f48b3dc58ecf854d6929fb3f8691..1ede4bffc94c5010ec17b5f58597f7b318c01d02 100644 --- a/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt +++ b/app/src/main/java/foundation/e/apps/install/workmanager/AppInstallProcessor.kt @@ -97,7 +97,7 @@ class AppInstallProcessor @Inject constructor( it.contentRating = application.contentRating } - if (appInstall.type == Type.PWA) { + if (appInstall.type == Type.PWA || application.isSystemApp) { appInstall.downloadURLList = mutableListOf(application.url) } diff --git a/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt index 474308feee5eb73de4f517a430c8a8f950865c0e..d1c94c49999004a210933e2a7a1bfd18ec2c7126 100644 --- a/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/MainActivityViewModel.kt @@ -39,6 +39,7 @@ import foundation.e.apps.data.ecloud.EcloudRepository import foundation.e.apps.data.enums.User import foundation.e.apps.data.enums.isInitialized import foundation.e.apps.data.enums.isUnFiltered +import foundation.e.apps.data.gitlab.SystemAppsUpdatesRepository import foundation.e.apps.data.install.AppManagerWrapper import foundation.e.apps.data.install.models.AppInstall import foundation.e.apps.data.parentalcontrol.fdroid.FDroidAntiFeatureRepository @@ -64,6 +65,7 @@ class MainActivityViewModel @Inject constructor( private val gPlayContentRatingRepository: GPlayContentRatingRepository, private val fDroidAntiFeatureRepository: FDroidAntiFeatureRepository, private val appInstallProcessor: AppInstallProcessor, + private val systemAppsUpdatesRepository: SystemAppsUpdatesRepository, ) : ViewModel() { val tocStatus: LiveData = appLoungeDataStore.tocStatus.asLiveData() @@ -247,6 +249,12 @@ class MainActivityViewModel @Inject constructor( } } + fun updateEligibleSystemAppsList() { + viewModelScope.launch { + systemAppsUpdatesRepository.fetchAllEligibleApps() + } + } + fun getAppNameByPackageName(packageName: String): String { return appLoungePackageManager.getAppNameFromPackageName(packageName) } diff --git a/app/src/main/java/foundation/e/apps/ui/application/model/ApplicationScreenshotsRVAdapter.kt b/app/src/main/java/foundation/e/apps/ui/application/model/ApplicationScreenshotsRVAdapter.kt index 9fefd43d0ea72d6622cd0605f48eca66ab751214..b9d452f3900dbcc481d15012fe447144bc647cca 100644 --- a/app/src/main/java/foundation/e/apps/ui/application/model/ApplicationScreenshotsRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/ui/application/model/ApplicationScreenshotsRVAdapter.kt @@ -58,6 +58,7 @@ class ApplicationScreenshotsRVAdapter( Origin.GPLAY -> { imageView.load(oldList[position]) } + else -> {} } imageView.setOnClickListener { val action = diff --git a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt index 43cd6685181d946d211d9f8d86ecc6fe5a2b9280..dd5010beebd563ebcff9dd5f70f9016251ea0300 100644 --- a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt +++ b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListRVAdapter.kt @@ -20,8 +20,10 @@ package foundation.e.apps.ui.applicationlist import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.ImageView import androidx.core.content.ContextCompat import androidx.core.view.children +import androidx.core.view.isVisible import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -175,10 +177,25 @@ class ApplicationListRVAdapter( placeholder(shimmerDrawable) } } + Origin.GITLAB -> { + setSystemAppIcon(appIcon, searchApp) + } else -> Timber.wtf("${searchApp.package_name} is from an unknown origin") } } + private fun setSystemAppIcon(imageView: ImageView, app: Application) { + if (!app.isSystemApp) return + try { + imageView.run { + setImageDrawable(context.packageManager.getApplicationIcon(app.package_name)) + } + } catch (e: Exception) { + Timber.w("Icon could not be set for system app - ${app.package_name} - ${e.message}") + e.printStackTrace() + } + } + private fun ApplicationListItemBinding.updateAppInfo(searchApp: Application) { appTitle.text = searchApp.name appInfoFetchViewModel.getAuthorName(searchApp).observe(lifecycleOwner!!) { @@ -187,6 +204,11 @@ class ApplicationListRVAdapter( } private fun ApplicationListItemBinding.updateRating(searchApp: Application) { + if (searchApp.isSystemApp) { + iconStar.isVisible = false + appRating.isVisible = false + return + } if (searchApp.ratings.usageQualityScore != -1.0) { appRating.text = searchApp.ratings.usageQualityScore.toString() } else { @@ -198,6 +220,10 @@ class ApplicationListRVAdapter( searchApp: Application, view: View ) { + if (searchApp.isSystemApp) { + appPrivacyScoreLayout.isVisible = false + return + } if (searchApp.ratings.privacyScore != -1.0) { appPrivacyScore.text = view.context.getString( R.string.privacy_rating_out_of, @@ -219,6 +245,9 @@ class ApplicationListRVAdapter( searchApp: Application, view: View ) { + if (searchApp.isSystemApp) { + return + } val catText = searchApp.category.ifBlank { optionalCategory } val action = when (currentDestinationId) { R.id.applicationListFragment -> { diff --git a/app/src/main/java/foundation/e/apps/ui/updates/UpdatesFragment.kt b/app/src/main/java/foundation/e/apps/ui/updates/UpdatesFragment.kt index fc68ab0b2bd4936dec0f16ae028026f1e04d0f12..e0ced31ebe9352334aede92a850352b20c99a531 100644 --- a/app/src/main/java/foundation/e/apps/ui/updates/UpdatesFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/updates/UpdatesFragment.kt @@ -133,7 +133,14 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), ApplicationI private fun observeUpdateList(listAdapter: ApplicationListRVAdapter?) { updatesViewModel.updatesList.observe(viewLifecycleOwner) { - listAdapter?.setData(it.first) + + val updateList = it.first + + // Put system apps on top + val appsToDisplay = updateList.filter { it.isSystemApp } + + updateList.filter { !it.isSystemApp } + + listAdapter?.setData(appsToDisplay) if (!isDownloadObserverAdded) { handleStateNoUpdates(it.first) observeDownloadList() 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? { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 85f7f2bb8b342ba1428f6fb71f66d6f9c5cb4007..993dcea40961870a935c2ddf8d6a04b3c05ea0bf 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -120,6 +120,7 @@ Open Source apps Some proprietary apps may also have an Open Source version. Whenever this happens App Lounge shows the Open Source version only, in order to avoid duplicates. Downloading… + System app Additional file for %s Having troubles? https://doc.e.foundation/support-topics/app_lounge_troubleshooting diff --git a/app/src/test/java/foundation/e/apps/UpdateManagerImptTest.kt b/app/src/test/java/foundation/e/apps/UpdateManagerImptTest.kt index 214e3ee21b47515b8abc0a11bfc0999feaa563ee..e22dbe538ca0f88a1b35c6256eb1b62329d75706 100644 --- a/app/src/test/java/foundation/e/apps/UpdateManagerImptTest.kt +++ b/app/src/test/java/foundation/e/apps/UpdateManagerImptTest.kt @@ -30,6 +30,7 @@ import foundation.e.apps.data.fdroid.FdroidRepository import foundation.e.apps.data.application.ApplicationRepository import foundation.e.apps.data.application.search.SearchApi import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.gitlab.SystemAppsUpdatesRepository import foundation.e.apps.data.updates.UpdatesManagerImpl import foundation.e.apps.util.MainCoroutineRule import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -78,6 +79,9 @@ class UpdateManagerImptTest { @Mock private lateinit var fdroidRepository: FdroidRepository + @Mock + private lateinit var systemAppsUpdatesRepository: SystemAppsUpdatesRepository + val authData = AuthData("e@e.email", "AtadyMsIAtadyM") @Before @@ -93,27 +97,40 @@ class UpdateManagerImptTest { faultyAppRepository, preferenceModule, fdroidRepository, - blockedAppRepository + blockedAppRepository, + systemAppsUpdatesRepository, ) } + private fun getSystemApps(status: Status = Status.UPDATABLE) = mutableListOf( + Application( + status = status, + name = "Demo Four", + package_name = "foundation.e.demofour", + origin = Origin.GITLAB, + filterLevel = FilterLevel.NONE + ) + ) + @Test fun getUpdateWhenUpdateIsAvailable() = runTest { val gplayApps = getGplayApps() val openSourceApps = getOpenSourceApps(Status.UPDATABLE) + val systemAppUpdates = getSystemApps() val openSourceUpdates = Pair(openSourceApps, ResultStatus.OK) val gplayUpdates = Pair(gplayApps, ResultStatus.OK) setupMockingForFetchingUpdates( openSourceUpdates, - gplayUpdates + gplayUpdates, + systemAppUpdates ) val updateResult = updatesManagerImpl.getUpdates(authData) System.out.println("===> updates: ${updateResult.first.map { it.package_name }}") - assertEquals("fetchUpdate", 2, updateResult.first.size) + assertEquals("fetchUpdate", 3, updateResult.first.size) } private fun getGplayApps(status: Status = Status.UPDATABLE) = mutableListOf( @@ -140,6 +157,8 @@ class UpdateManagerImptTest { val authData = AuthData("e@e.email", "AtadyMsIAtadyM") pkgManagerModule.applicationInfo.clear() + setupMockingSystemApps() + val updateResult = updatesManagerImpl.getUpdates(authData) System.out.println("===> updates: ${updateResult.first.map { it.package_name }}") @@ -150,13 +169,15 @@ class UpdateManagerImptTest { fun getUpdateWhenUpdateIsUnavailable() = runTest { val gplayApps = getGplayApps(Status.INSTALLED) val openSourceApps = getOpenSourceApps(Status.INSTALLED) + val systemAppUpdates = getSystemApps(Status.INSTALLED) val openSourceUpdates = Pair(openSourceApps, ResultStatus.OK) val gplayUpdates = Pair(gplayApps, ResultStatus.OK) setupMockingForFetchingUpdates( openSourceUpdates, - gplayUpdates + gplayUpdates, + systemAppUpdates, ) val updateResult = updatesManagerImpl.getUpdates(authData) @@ -169,36 +190,59 @@ class UpdateManagerImptTest { fun getUpdateWhenUpdateHasOnlyForOpenSourceApps() = runTest { val gplayApps = getGplayApps(Status.INSTALLED) val openSourceApps = getOpenSourceApps(Status.UPDATABLE) + val systemAppUpdates = getSystemApps(Status.INSTALLED) val openSourceUpdates = Pair(openSourceApps, ResultStatus.OK) val gplayUpdates = Pair(gplayApps, ResultStatus.OK) setupMockingForFetchingUpdates( openSourceUpdates, - gplayUpdates + gplayUpdates, + systemAppUpdates, ) val updateResult = updatesManagerImpl.getUpdates(authData) System.out.println("===> updates: ${updateResult.first.map { it.package_name }}") - assertFalse("fetchupdate", updateResult.first.any { it.origin == Origin.GPLAY }) + assertFalse("fetchupdate", updateResult.first.any { it.origin != Origin.CLEANAPK }) } @Test fun getUpdateWhenUpdateHasOnlyForGplayApps() = runTest { val gplayApps = getGplayApps(Status.UPDATABLE) val openSourceApps = getOpenSourceApps(Status.INSTALLED) + val systemAppUpdates = getSystemApps(Status.INSTALLED) val openSourceUpdates = Pair(openSourceApps, ResultStatus.OK) val gplayUpdates = Pair(gplayApps, ResultStatus.OK) setupMockingForFetchingUpdates( openSourceUpdates, - gplayUpdates + gplayUpdates, + systemAppUpdates, + ) + + val updateResult = updatesManagerImpl.getUpdates(authData) + assertFalse("fetchupdate", updateResult.first.any { it.origin != Origin.GPLAY }) + } + + @Test + fun getUpdateWhenUpdateHasOnlySystemApps() = runTest { + val gplayApps = getGplayApps(Status.INSTALLED) + val openSourceApps = getOpenSourceApps(Status.INSTALLED) + val systemAppUpdates = getSystemApps(Status.UPDATABLE) + + val openSourceUpdates = Pair(openSourceApps, ResultStatus.OK) + val gplayUpdates = Pair(gplayApps, ResultStatus.OK) + + setupMockingForFetchingUpdates( + openSourceUpdates, + gplayUpdates, + systemAppUpdates, ) val updateResult = updatesManagerImpl.getUpdates(authData) - assertFalse("fetchupdate", updateResult.first.any { it.origin == Origin.CLEANAPK }) + assertFalse("fetchupdate", updateResult.first.any { it.origin != Origin.GITLAB }) } @Test @@ -274,15 +318,17 @@ class UpdateManagerImptTest { fun getUpdatesOSSWhenUpdateIsAvailable() = runTest { val openSourceApps = getOpenSourceApps(Status.UPDATABLE) val gPlayApps = getGplayApps(Status.UPDATABLE) + val systemAppUpdates = getSystemApps() val openSourceUpdates = Pair(openSourceApps, ResultStatus.OK) val gplayUpdates = Pair(gPlayApps, ResultStatus.OK) - setupMockingForFetchingUpdates(openSourceUpdates, gplayUpdates) + setupMockingForFetchingUpdates(openSourceUpdates, gplayUpdates, systemAppUpdates) val updateResult = updatesManagerImpl.getUpdatesOSS() - assertEquals("UpdateOSS", 1, updateResult.first.size) + assertEquals("UpdateOSS", 2, updateResult.first.size) assertEquals("UpdateOSS", Origin.CLEANAPK, updateResult.first[0].origin) + assertEquals("UpdateOSS", Origin.GITLAB, updateResult.first[1].origin) } @Test @@ -317,6 +363,7 @@ class UpdateManagerImptTest { private suspend fun setupMockingForFetchingUpdates( openSourceUpdates: Pair, ResultStatus>, gplayUpdates: Pair, ResultStatus>, + systemAppUpdates: MutableList = mutableListOf(), selectedApplicationSources: List = mutableListOf( SearchApi.APP_TYPE_ANY, SearchApi.APP_TYPE_OPEN, @@ -334,6 +381,8 @@ class UpdateManagerImptTest { Mockito.`when`(applicationRepository.getSelectedAppTypes()) .thenReturn(selectedApplicationSources) + setupMockingSystemApps(systemAppUpdates) + if (gplayUpdates.first.isNotEmpty()) { Mockito.`when`( applicationRepository.getApplicationDetails( @@ -357,4 +406,11 @@ class UpdateManagerImptTest { ).thenReturn(Pair(Application(), ResultStatus.TIMEOUT)) } } + + private suspend fun setupMockingSystemApps( + systemAppUpdates: MutableList = mutableListOf() + ) { + Mockito.`when`(systemAppsUpdatesRepository.getSystemUpdates()) + .thenReturn(systemAppUpdates) + } }