diff --git a/app/src/main/java/foundation/e/apps/api/fdroid/FdroidRepository.kt b/app/src/main/java/foundation/e/apps/api/fdroid/FdroidRepository.kt index beba5d5b2c7e633c16652c106a8640ea34b89c6b..99ee0f1e762ec8205627b18af0a58b16fed5a5ef 100644 --- a/app/src/main/java/foundation/e/apps/api/fdroid/FdroidRepository.kt +++ b/app/src/main/java/foundation/e/apps/api/fdroid/FdroidRepository.kt @@ -2,6 +2,7 @@ package foundation.e.apps.api.fdroid import android.content.Context import foundation.e.apps.api.cleanapk.ApkSignatureManager +import foundation.e.apps.api.fdroid.models.BuildInfo import foundation.e.apps.api.fdroid.models.FdroidEntity import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.utils.enums.Origin @@ -35,6 +36,10 @@ class FdroidRepository @Inject constructor( } } + suspend fun getBuildVersionInfo(packageName: String): List? { + return fdroidApi.getFdroidInfoForPackage(packageName).body()?.builds + } + override suspend fun getAuthorName(fusedApp: FusedApp): String { if (fusedApp.author != UNKNOWN || fusedApp.origin != Origin.CLEANAPK) { return fusedApp.author.ifEmpty { UNKNOWN } diff --git a/app/src/main/java/foundation/e/apps/api/fdroid/models/BuildInfo.kt b/app/src/main/java/foundation/e/apps/api/fdroid/models/BuildInfo.kt new file mode 100644 index 0000000000000000000000000000000000000000..047395ded9c6824c172778f5ce03f479626eba9e --- /dev/null +++ b/app/src/main/java/foundation/e/apps/api/fdroid/models/BuildInfo.kt @@ -0,0 +1,37 @@ +/* + * 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.api.fdroid.models + +import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty + +@JsonIgnoreProperties(ignoreUnknown = true) +class BuildInfo() { + var versionCode: String = "" + var versionName: String = "" + + @JsonCreator + constructor( + @JsonProperty("versionCode") versionCode: String?, + @JsonProperty("versionName") versionName: String?, + ) : this() { + this.versionCode = versionCode ?: "" + this.versionName = versionName ?: "" + } +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/apps/api/fdroid/models/FdroidApiModel.kt b/app/src/main/java/foundation/e/apps/api/fdroid/models/FdroidApiModel.kt index e797823adc17e7858cd6b5159304d193bb7848c3..e25eece5918425c7a0eabd51a2755238a2a758e2 100644 --- a/app/src/main/java/foundation/e/apps/api/fdroid/models/FdroidApiModel.kt +++ b/app/src/main/java/foundation/e/apps/api/fdroid/models/FdroidApiModel.kt @@ -19,9 +19,14 @@ import com.fasterxml.jackson.annotation.JsonProperty @JsonIgnoreProperties(ignoreUnknown = true) class FdroidApiModel() { var authorName: String = "" + var builds: List = mutableListOf() @JsonCreator - constructor(@JsonProperty("AuthorName") AuthorName: String?) : this() { + constructor( + @JsonProperty("AuthorName") AuthorName: String?, + @JsonProperty("Builds") Builds: List?, + ) : this() { this.authorName = AuthorName ?: "" + this.builds = Builds ?: emptyList() } } diff --git a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt index 33e061c162aea417085482e4cf9d3c863095ea23..67948798b39cc2a32e1941d98d47a352c88f7110 100644 --- a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt +++ b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIImpl.kt @@ -546,6 +546,9 @@ class FusedAPIImpl @Inject constructor( fusedDownload.downloadURLList = list } + suspend fun getOSSDownloadInfo(id: String, version: String?) = + cleanAPKRepository.getDownloadInfo(id, version) + suspend fun getPWAApps(category: String): ResultSupreme> { val list = mutableListOf() val status = runCodeBlockWithTimeout({ diff --git a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt index 537b533c0c949137617f649f4cd91fd5a247151d..7cd9bdcf13832066a93c2ef8145326019a47ac4f 100644 --- a/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt +++ b/app/src/main/java/foundation/e/apps/api/fused/FusedAPIRepository.kt @@ -134,6 +134,9 @@ class FusedAPIRepository @Inject constructor(private val fusedAPIImpl: FusedAPII ) } + suspend fun getOSSDownloadInfo(id: String, version: String? = null) = + fusedAPIImpl.getOSSDownloadInfo(id, version) + suspend fun getOnDemandModule( authData: AuthData, packageName: String, diff --git a/app/src/main/java/foundation/e/apps/manager/pkg/PkgManagerModule.kt b/app/src/main/java/foundation/e/apps/manager/pkg/PkgManagerModule.kt index 4165042043616cceef38383841207f8ca70b2d71..1b99aecb1fd45ed0e1f86fff17fa979343d358fe 100644 --- a/app/src/main/java/foundation/e/apps/manager/pkg/PkgManagerModule.kt +++ b/app/src/main/java/foundation/e/apps/manager/pkg/PkgManagerModule.kt @@ -137,6 +137,29 @@ class PkgManagerModule @Inject constructor( } } + /** + * For an installed app, get the path to the base.apk. + */ + fun getBaseApkPath(packageName: String): String { + val packageInfo = getPackageInfo(packageName) + return packageInfo?.applicationInfo?.publicSourceDir ?: "" + } + + fun getVersionCode(packageName: String): String { + val packageInfo = getPackageInfo(packageName) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + packageInfo?.longVersionCode?.toString() ?: "" + } else { + @Suppress("DEPRECATION") + packageInfo?.versionCode?.toString() ?: "" + } + } + + fun getVersionName(packageName: String): String { + val packageInfo = getPackageInfo(packageName) + return packageInfo?.versionName?.toString() ?: "" + } + /** * Installs the given package using system API * @param list List of [File] to be written to install session. diff --git a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerImpl.kt b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerImpl.kt index 97ef1bdd6531a6234977d4f08991bd5851031824..7c4a002b8507e9b19a4ac533e2b3104b66f0b3f8 100644 --- a/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerImpl.kt +++ b/app/src/main/java/foundation/e/apps/updates/manager/UpdatesManagerImpl.kt @@ -22,18 +22,20 @@ import android.content.Context import android.content.pm.ApplicationInfo import com.aurora.gplayapi.data.models.AuthData import dagger.hilt.android.qualifiers.ApplicationContext +import foundation.e.apps.api.cleanapk.ApkSignatureManager import foundation.e.apps.api.faultyApps.FaultyAppRepository +import foundation.e.apps.api.fdroid.FdroidRepository import foundation.e.apps.api.fused.FusedAPIImpl.Companion.APP_TYPE_ANY import foundation.e.apps.api.fused.FusedAPIRepository import foundation.e.apps.api.fused.data.FusedApp import foundation.e.apps.manager.pkg.PkgManagerModule -import foundation.e.apps.utils.Constants import foundation.e.apps.utils.enums.Origin import foundation.e.apps.utils.enums.ResultStatus import foundation.e.apps.utils.enums.Status import foundation.e.apps.utils.enums.isUnFiltered import foundation.e.apps.utils.modules.PreferenceManagerModule import javax.inject.Inject +import timber.log.Timber class UpdatesManagerImpl @Inject constructor( @ApplicationContext private val context: Context, @@ -41,6 +43,7 @@ class UpdatesManagerImpl @Inject constructor( private val fusedAPIRepository: FusedAPIRepository, private val faultyAppRepository: FaultyAppRepository, private val preferenceManagerModule: PreferenceManagerModule, + private val fdroidRepository: FdroidRepository, ) { companion object { @@ -61,10 +64,17 @@ class UpdatesManagerImpl @Inject constructor( val openSourceInstalledApps = getOpenSourceInstalledApps().toMutableList() val gPlayInstalledApps = getGPlayInstalledApps().toMutableList() - val otherStoreApps = getAppsFromOtherStores() - if (preferenceManagerModule.shouldUpdateAppsFromOtherStores()) { - openSourceInstalledApps.addAll(otherStoreApps) + val otherStoresInstalledApps = getAppsFromOtherStores().toMutableList() + + // This list is based on app signatures + val updatableFDroidApps = + findPackagesMatchingFDroidSignatures(otherStoresInstalledApps) + + openSourceInstalledApps.addAll(updatableFDroidApps) + + otherStoresInstalledApps.removeAll(updatableFDroidApps) + gPlayInstalledApps.addAll(otherStoresInstalledApps) } // Get open source app updates @@ -78,12 +88,6 @@ class UpdatesManagerImpl @Inject constructor( }, updateList) } - if (preferenceManagerModule.shouldUpdateAppsFromOtherStores()) { - val updateListFromFDroid = updateList.map { it.package_name } - val otherStoreAppsForGPlay = otherStoreApps - updateListFromFDroid.toSet() - gPlayInstalledApps.addAll(otherStoreAppsForGPlay) - } - // Get GPlay app updates if (getApplicationCategoryPreference().contains(APP_TYPE_ANY) && gPlayInstalledApps.isNotEmpty()) { @@ -107,10 +111,14 @@ class UpdatesManagerImpl @Inject constructor( val openSourceInstalledApps = getOpenSourceInstalledApps().toMutableList() - val otherStoreApps = getAppsFromOtherStores() - if (preferenceManagerModule.shouldUpdateAppsFromOtherStores()) { - openSourceInstalledApps.addAll(otherStoreApps) + val otherStoresInstalledApps = getAppsFromOtherStores().toMutableList() + + // This list is based on app signatures + val updatableFDroidApps = + findPackagesMatchingFDroidSignatures(otherStoresInstalledApps) + + openSourceInstalledApps.addAll(updatableFDroidApps) } if (openSourceInstalledApps.isNotEmpty()) { @@ -192,6 +200,128 @@ class UpdatesManagerImpl @Inject constructor( return apiResult.second } + /** + * Takes a list of package names and for the apps present on F-Droid, + * returns key value pairs of package names and their signatures. + * + * The signature for an app corresponds to the version currently + * installed on the device. + * If the current installed version for an app is (say) 7, then even if + * the latest version is 10, we try to find the signature of version 7. + * If signature for version 7 of the app is unavailable, then we put blank. + * + * If none of the apps mentioned in [installedPackageNames] are present on F-Droid, + * then it returns an empty Map. + * + * Map is String : String = package name : signature + */ + private suspend fun getFDroidAppsAndSignatures(installedPackageNames: List): Map { + val appsAndSignatures = hashMapOf() + for (packageName in installedPackageNames) { + val cleanApkFusedApp = fusedAPIRepository.getCleanapkAppDetails(packageName).first + if (cleanApkFusedApp.package_name.isBlank()) { + continue + } + appsAndSignatures[packageName] = getPgpSignature(cleanApkFusedApp) + } + return appsAndSignatures + } + + private suspend fun getPgpSignature(cleanApkFusedApp: FusedApp): String { + val installedVersionSignature = calculateSignatureVersion(cleanApkFusedApp) + + val downloadInfo = + fusedAPIRepository + .getOSSDownloadInfo(cleanApkFusedApp._id, installedVersionSignature) + .body()?.download_data + + val pgpSignature = downloadInfo?.signature ?: "" + + Timber.i( + "Signature calculated for : ${cleanApkFusedApp.package_name}, " + + "signature version: ${installedVersionSignature}, " + + "is sig blank: ${pgpSignature.isBlank()}" + ) + + return downloadInfo?.signature ?: "" + } + + /** + * Returns list of packages whose signature matches with the available listing on F-Droid. + * + * Example: If Element (im.vector.app) is installed from ApkMirror, then it's signature + * will not match with the version of Element on F-Droid. So if Element is present + * in [installedPackageNames], it will not be present in the list returned by this method. + */ + private suspend fun findPackagesMatchingFDroidSignatures( + installedPackageNames: List, + ): List { + val fDroidAppsAndSignatures = getFDroidAppsAndSignatures(installedPackageNames) + + val fDroidUpdatablePackageNames = fDroidAppsAndSignatures.filter { + // For each installed app also present on F-droid, check signature of base APK. + val baseApkPath = pkgManagerModule.getBaseApkPath(it.key) + ApkSignatureManager.verifyFdroidSignature(context, baseApkPath, it.value) + }.map { it.key } + + return fDroidUpdatablePackageNames + } + + /** + * Get signature version for the installed version of the app. + * A signature version is like "update_XX" where XX is a 2 digit number. + * + * Example: + * The installed versionCode of an app is (say) 7. + * The latest available version is (say) 10, we need to update to this version. + * The latest signature version is (say) "update_33". + * Available builds of F-droid are (say): + * version 10 + * version 9 + * version 8 + * version 7 + * ... + * Index of version 7 from top is 3 (index of version 10 is 0). + * So the corresponding signature version will be "update_(33-3)" = "update_30" + */ + private suspend fun calculateSignatureVersion(latestCleanapkApp: FusedApp): String { + val packageName = latestCleanapkApp.package_name + val latestSignatureVersion = latestCleanapkApp.latest_downloaded_version + + Timber.i("Latest signature version for $packageName : $latestSignatureVersion") + + val installedVersionCode = pkgManagerModule.getVersionCode(packageName) + val installedVersionName = pkgManagerModule.getVersionName(packageName) + + Timber.i("Calculate signature for $packageName : $installedVersionCode, $installedVersionName") + + val latestSignatureVersionNumber = try { + latestSignatureVersion.split("_")[1].toInt() + } catch (e: Exception) { + return "" + } + + + // Received list has build info of the latest version at the bottom. + // We want it at the top. + val builds = fdroidRepository.getBuildVersionInfo(packageName)?.asReversed() ?: return "" + + val matchingIndex = builds.find { + it.versionCode == installedVersionCode && it.versionName == installedVersionName + }?.run { + builds.indexOf(this) + }?: return "" + + Timber.i("Build info match at index: $matchingIndex") + + /* If latest latest signature version is (say) "update_33" + * corresponding to (say) versionCode 10, and we need to find signature + * version of (say) versionCode 7, then we calculate signature version as: + * "update_" + [33 (latestSignatureVersionNumber) - 3 (i.e. matchingIndex)] = "update_30" + */ + return "update_${latestSignatureVersionNumber - matchingIndex}" + } + fun getApplicationCategoryPreference(): List { return fusedAPIRepository.getApplicationCategoryPreference() }