diff --git a/app/src/main/java/foundation/e/apps/data/application/apps/AppsApiImpl.kt b/app/src/main/java/foundation/e/apps/data/application/apps/AppsApiImpl.kt index 9b514e4e52074f09b8cb6a1ce3acd7f622e88e3c..0bc0959902556dc9f94b72fabe440adffba83d4d 100644 --- a/app/src/main/java/foundation/e/apps/data/application/apps/AppsApiImpl.kt +++ b/app/src/main/java/foundation/e/apps/data/application/apps/AppsApiImpl.kt @@ -21,6 +21,7 @@ package foundation.e.apps.data.application.apps import foundation.e.apps.data.Stores import foundation.e.apps.data.application.ApplicationDataManager import foundation.e.apps.data.application.data.Application +import foundation.e.apps.data.cleanapk.repositories.CleanApkRepository import foundation.e.apps.data.enums.FilterLevel import foundation.e.apps.data.enums.ResultStatus import foundation.e.apps.data.enums.Source @@ -29,6 +30,8 @@ import foundation.e.apps.data.handleNetworkResult import foundation.e.apps.data.playstore.PlayStoreRepository import foundation.e.apps.domain.model.install.Status import foundation.e.apps.ui.applicationlist.ApplicationDiffUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import javax.inject.Inject class AppsApiImpl @Inject constructor( @@ -38,7 +41,9 @@ class AppsApiImpl @Inject constructor( override suspend fun getCleanapkAppDetails(packageName: String): Pair { var application = Application() val result = handleNetworkResult { - application = stores.getStore(Source.OPEN_SOURCE)?.getAppDetails(packageName) ?: Application() + application = withContext(Dispatchers.IO) { + stores.getStore(Source.OPEN_SOURCE)?.getAppDetails(packageName) ?: Application() + } application.source = Source.OPEN_SOURCE application.updateType() application.updateFilterLevel() @@ -88,10 +93,17 @@ class AppsApiImpl @Inject constructor( packageNameList: List, ): Pair, ResultStatus> { val status = ResultStatus.OK - val applicationList = mutableListOf() - - for (packageName in packageNameList) { - applicationList.add(stores.getStore(Source.OPEN_SOURCE)?.getAppDetails(packageName) ?: Application()) + val applicationList = withContext(Dispatchers.IO) { + val store = stores.getStore(Source.OPEN_SOURCE) + if (store is CleanApkRepository) { + store.getAppDetailsForPackages(packageNameList) + } else { + val list = mutableListOf() + for (packageName in packageNameList) { + list.add(store?.getAppDetails(packageName) ?: Application()) + } + list + } } return Pair(applicationList, status) @@ -104,7 +116,9 @@ class AppsApiImpl @Inject constructor( val playStore = stores.getStore(Source.PLAY_STORE) as? PlayStoreRepository val response = handleNetworkResult { - playStore?.getAppsDetails(packageNameList).orEmpty() + withContext(Dispatchers.IO) { + playStore?.getAppsDetails(packageNameList).orEmpty() + } } val apps = response.data ?: emptyList() for (app in apps) { @@ -144,7 +158,9 @@ class AppsApiImpl @Inject constructor( val store = stores.getStore(source) ?: throw IllegalStateException("Could not get store") - application = store.getAppDetails(packageName) + application = withContext(Dispatchers.IO) { + store.getAppDetails(packageName) + } application.let { applicationDataManager.updateStatus(it) it.source = 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 88e91c31c1d92530b2648e5aab1ba0ed6d20931e..57f500d00296f0dad15ab0b1a2a92d8ac94e3b28 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 @@ -23,6 +23,8 @@ import foundation.e.apps.data.cleanapk.CleanApkDownloadInfoFetcher import foundation.e.apps.data.enums.Source import foundation.e.apps.data.handleNetworkResult import foundation.e.apps.data.installation.model.AppInstall +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import javax.inject.Inject class DownloadInfoApiImpl @Inject constructor( @@ -127,6 +129,8 @@ class DownloadInfoApiImpl @Inject constructor( } override suspend fun getOSSDownloadInfo(id: String, version: String?) = - (appSources.cleanApkAppsRepo as CleanApkDownloadInfoFetcher) - .getDownloadInfo(id, version) + withContext(Dispatchers.IO) { + (appSources.cleanApkAppsRepo as CleanApkDownloadInfoFetcher) + .getDownloadInfo(id, version) + } } diff --git a/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkAppsRepository.kt b/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkAppsRepository.kt index 0dc547416f3ca0aee9c54a143de5aa6057dc14e2..edf7c40dd5e931849c9bfa4c1472b9e676cd217d 100644 --- a/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkAppsRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkAppsRepository.kt @@ -30,6 +30,9 @@ import foundation.e.apps.data.cleanapk.data.download.Download import foundation.e.apps.data.cleanapk.data.search.Search import foundation.e.apps.data.enums.Source import foundation.e.apps.data.system.SystemInfoProvider +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import retrofit2.Response import javax.inject.Inject @@ -106,6 +109,44 @@ class CleanApkAppsRepository @Inject constructor( } ?: Application() } + override suspend fun getAppDetailsForPackages(packageNames: List): List { + return coroutineScope { + if (packageNames.isEmpty()) { + return@coroutineScope emptyList() + } + + val response = cleanApkRetrofit.checkAvailablePackages( + packages = packageNames, + architectures = SystemInfoProvider.getSupportedArchitectureList() + ) + val search = response.body() + if (!response.isSuccessful || search?.success != true) { + return@coroutineScope packageNames.map { + async { getAppDetails(it) } + }.awaitAll() + } + + val appsByPackage = search.apps.associateBy { it.package_name } + return@coroutineScope packageNames.map { packageName -> + val app = appsByPackage[packageName] + if (app == null || app._id.isBlank()) { + async { getAppDetails(packageName) } + } else { + async { + val appResponse = cleanApkRetrofit.getAppOrPWADetailsByID( + id = app._id, + architectures = SystemInfoProvider.getSupportedArchitectureList(), + type = null + ) + appResponse.body()?.app?.let { + it.copy(source = if (it.is_pwa) Source.PWA else Source.OPEN_SOURCE) + } ?: Application() + } + } + }.awaitAll() + } + } + override suspend fun getSearchResults(pattern: String): List { return cleanApkSearchHelper.getSearchResults( keyword = pattern, diff --git a/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkPwaRepository.kt b/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkPwaRepository.kt index 1cedff4721b0a54b525b11560f74366d8da243a0..5eb6ec04b12e8a1d74979c18f4d4325f5b78f4c8 100644 --- a/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkPwaRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkPwaRepository.kt @@ -31,6 +31,9 @@ import foundation.e.apps.data.cleanapk.data.categories.Categories import foundation.e.apps.data.cleanapk.data.search.Search import foundation.e.apps.data.enums.Source import foundation.e.apps.data.enums.Type +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import retrofit2.Response import javax.inject.Inject @@ -100,6 +103,47 @@ class CleanApkPwaRepository @Inject constructor( } ?: Application() } + override suspend fun getAppDetailsForPackages(packageNames: List): List { + return coroutineScope { + if (packageNames.isEmpty()) { + return@coroutineScope emptyList() + } + + val response = cleanApkRetrofit.checkAvailablePackages( + packageNames, + CleanApkRetrofit.APP_TYPE_PWA + ) + val search = response.body() + if (!response.isSuccessful || search?.success != true) { + return@coroutineScope packageNames.map { + async { getAppDetails(it) } + }.awaitAll() + } + + val appsByPackage = search.apps.associateBy { it.package_name } + return@coroutineScope packageNames.map { packageName -> + val app = appsByPackage[packageName] + if (app == null || app._id.isBlank()) { + async { getAppDetails(packageName) } + } else { + async { + val appResponse = cleanApkRetrofit.getAppOrPWADetailsByID(app._id, null, null) + appResponse.body()?.app?.let { + if (it.is_pwa) { + it.copy( + source = Source.PWA, + type = Type.PWA + ) + } else { + it + } + } ?: Application() + } + } + }.awaitAll() + } + } + override suspend fun getSearchResults(pattern: String): List { return cleanApkSearchHelper.getSearchResults( keyword = pattern, diff --git a/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkRepository.kt b/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkRepository.kt index 3d0c19e213353397d0bf621dbc02107723eca497..4fda8b90f0b1e13f759b9698d43ee675bb716c24 100644 --- a/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/cleanapk/repositories/CleanApkRepository.kt @@ -19,8 +19,12 @@ package foundation.e.apps.data.cleanapk.repositories import foundation.e.apps.data.StoreRepository +import foundation.e.apps.data.application.data.Application import foundation.e.apps.data.cleanapk.data.categories.Categories import foundation.e.apps.data.cleanapk.data.search.Search +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope import retrofit2.Response const val NUMBER_OF_ITEMS = 20 @@ -30,4 +34,12 @@ interface CleanApkRepository : StoreRepository { suspend fun getAppsByCategory(category: String, paginationParameter: Any? = null): Response suspend fun getCategories(): Response suspend fun checkAvailablePackages(packageNames: List): Response + + suspend fun getAppDetailsForPackages(packageNames: List): List { + return coroutineScope { + packageNames.map { + async { getAppDetails(it) } + }.awaitAll() + } + } } diff --git a/app/src/main/java/foundation/e/apps/data/fdroid/FDroidRepository.kt b/app/src/main/java/foundation/e/apps/data/fdroid/FDroidRepository.kt index a9821c1a83813935155bd5ff98f2dbb4832b3080..a13c4ce99ad8f66bff316b9d049d8e84df33bd4d 100644 --- a/app/src/main/java/foundation/e/apps/data/fdroid/FDroidRepository.kt +++ b/app/src/main/java/foundation/e/apps/data/fdroid/FDroidRepository.kt @@ -8,6 +8,8 @@ import foundation.e.apps.data.fdroid.models.BuildInfo import foundation.e.apps.data.fdroid.models.FdroidApiModel import foundation.e.apps.data.fdroid.models.FdroidEntity import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import retrofit2.Response import timber.log.Timber import java.io.IOException @@ -42,7 +44,9 @@ class FDroidRepository @Inject constructor( } suspend fun getBuildVersionInfo(packageName: String): List { - return getFdroidApiResponse(packageName)?.body()?.builds ?: emptyList() + return withContext(Dispatchers.IO) { + getFdroidApiResponse(packageName)?.body()?.builds ?: emptyList() + } } override suspend fun getAuthorName(application: Application): String { 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 9ac4c773eb406be9a68ebb4cfc228a4583741461..59459aa7678cd3917bddbd27613f876787866569 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 @@ -33,6 +33,8 @@ import foundation.e.apps.data.handleNetworkResult import foundation.e.apps.data.install.pkg.AppLoungePackageManager import foundation.e.apps.data.system.SystemInfoProvider import foundation.e.apps.domain.model.install.Status +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import timber.log.Timber import javax.inject.Inject import javax.inject.Singleton @@ -160,17 +162,27 @@ class SystemAppsUpdatesRepository @Inject constructor( sdkLevel: Int, device: String, ): Application? { - val systemAppProject = systemAppProjectList.find { it.packageName == packageName } ?: return null - val detailsUrl = getReleaseDetailsUrl(systemAppProject, releaseType) ?: return null - - val systemAppInfo = getSystemAppInfo(packageName, detailsUrl) ?: return null - - return if (isSystemAppBlocked(systemAppInfo, sdkLevel, device)) { + val systemAppProject = systemAppProjectList.find { it.packageName == packageName } + val detailsUrl = systemAppProject?.let { + getReleaseDetailsUrl(it, releaseType) + } + val systemAppInfo = detailsUrl?.let { + getSystemAppInfo(packageName, it) + } + val isBlocked = systemAppInfo?.let { + isSystemAppBlocked(it, sdkLevel, device) + } == true + if (isBlocked) { Timber.e("Blocked system app: $packageName, details: $systemAppInfo") - null - } else { + } + + val application = if (systemAppInfo != null && !isBlocked) { systemAppInfo.toApplication(context) + } else { + null } + + return application } private suspend fun getSystemAppInfo( @@ -229,45 +241,66 @@ class SystemAppsUpdatesRepository @Inject constructor( } } - suspend fun getSystemUpdates(): List { + suspend fun getSystemUpdates(): List = coroutineScope { 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 fetchTasks = updatableApps.map { packageName -> + async { + fetchSystemAppUpdate(packageName, releaseType, sdkLevel, device) } + } + + fetchTasks.forEach { deferred -> + deferred.await()?.let { updateList.add(it) } + } - val result = handleNetworkResult { + return@coroutineScope updateList + } + + private suspend fun fetchSystemAppUpdate( + packageName: String, + releaseType: OsReleaseType, + sdkLevel: Int, + device: String, + ): Application? { + val installed = appLoungePackageManager.isInstalled(packageName) + + val result = if (installed) { + handleNetworkResult { getApplication( - it, + packageName, releaseType, sdkLevel, device, ) } + } else { + null + } + if (result != null) { if (!result.isSuccess()) { - Timber.e("Failed to get system app info for $it - ${result.message}") - return@forEach + Timber.e("Failed to get system app info for $packageName - ${result.message}") } + } - val app: Application = result.data ?: return@forEach - val appStatus = appLoungePackageManager.getPackageStatus(it, app.latest_version_code) - if (appStatus != Status.UPDATABLE) return@forEach - + val app: Application? = if (result?.isSuccess() == true) result.data else null + val appStatus = app?.let { appLoungePackageManager.getPackageStatus(packageName, it.latest_version_code) } + val updateApp = if (app != null && appStatus == Status.UPDATABLE) { app.run { applicationDataManager.updateStatus(this) source = Source.SYSTEM_APP - updateList.add(this) } + app + } else { + null } - - return updateList + return updateApp } } 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 e4fbbdbc0c415fce5718e67534a7764da7b92ee8..e51b6337a03cf1ce347f8b52d18168785750e300 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 @@ -35,8 +35,9 @@ import foundation.e.apps.data.install.pkg.AppLoungePackageManager import foundation.e.apps.domain.model.install.Status import foundation.e.apps.domain.preferences.AppPreferencesRepository import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext -import timber.log.Timber import javax.inject.Inject @Suppress("LongParameterList") @@ -59,20 +60,21 @@ class UpdatesManagerImpl @Inject constructor( private val userApplications: List get() = appLoungePackageManager.getAllUserApps() - suspend fun getUpdates(): Pair, ResultStatus> { - val updateList = mutableListOf() - var status = ResultStatus.OK - + @Suppress("LongMethod") + suspend fun getUpdates(): Pair, ResultStatus> = coroutineScope { val openSourceInstalledApps = getOpenSourceInstalledApps().toMutableList() val gPlayInstalledApps = getGPlayInstalledApps().toMutableList() if (appPreferencesRepository.shouldUpdateAppsFromOtherStores()) { withContext(Dispatchers.IO) { - val otherStoresInstalledApps = getAppsFromOtherStores().toMutableList() + val otherStoresInstalledApps = + getAppsFromOtherStores(openSourceInstalledApps, gPlayInstalledApps).toMutableList() + + val cleanApkAppsByPackage = getCleanApkDetailsByPackage(otherStoresInstalledApps) // This list is based on app signatures val updatableFDroidApps = - findPackagesMatchingFDroidSignatures(otherStoresInstalledApps) + findPackagesMatchingFDroidSignatures(otherStoresInstalledApps, cleanApkAppsByPackage) openSourceInstalledApps.addAll(updatableFDroidApps) @@ -89,52 +91,66 @@ class UpdatesManagerImpl @Inject constructor( blockedAppRepository.isBlockedApp(it) } - // Get open source app updates - if (openSourceInstalledApps.isNotEmpty()) { - status = getUpdatesFromApi({ - applicationRepository.getApplicationDetails( - openSourceInstalledApps, - Source.OPEN_SOURCE - ) - }, updateList) + val openSourceDeferred = if (openSourceInstalledApps.isNotEmpty()) { + async { + getUpdatesFromApi { + applicationRepository.getApplicationDetails( + openSourceInstalledApps, + Source.OPEN_SOURCE + ) + } + } + } else { + null } - // Get GPlay app updates - if (getApplicationCategoryPreference().contains(ApplicationRepository.APP_TYPE_ANY) && + val gplayDeferred = if (getApplicationCategoryPreference().contains(ApplicationRepository.APP_TYPE_ANY) && gPlayInstalledApps.isNotEmpty() ) { - val gplayStatus = getUpdatesFromApi({ - getGPlayUpdates( - gPlayInstalledApps - ) - }, updateList) - - /** - If any one of the sources is successful, status should be [ResultStatus.OK] - **/ - status = if (status == ResultStatus.OK) status else gplayStatus + async { + getUpdatesFromApi { + getGPlayUpdates(gPlayInstalledApps) + } + } + } else { + null + } + + val systemAppsDeferred = async { + getSystemAppUpdates() } - val systemApps = getSystemAppUpdates() + val openSourceResult = openSourceDeferred?.await() + val gplayResult = gplayDeferred?.await() + val systemApps = systemAppsDeferred.await() + + val updateList = mutableListOf() + updateList.addAll(openSourceResult?.first.orEmpty()) + updateList.addAll(gplayResult?.first.orEmpty()) val nonFaultyUpdateList = faultyAppRepository.removeFaultyApps(updateList) addSystemApps(updateList, nonFaultyUpdateList, systemApps) - return Pair(updateList, status) - } + var status = openSourceResult?.second ?: ResultStatus.OK + if (gplayResult != null && status != ResultStatus.OK) { + status = gplayResult.second + } - suspend fun getUpdatesOSS(): Pair, ResultStatus> { - val updateList = mutableListOf() - var status = ResultStatus.OK + return@coroutineScope Pair(updateList, status) + } + suspend fun getUpdatesOSS(): Pair, ResultStatus> = coroutineScope { val openSourceInstalledApps = getOpenSourceInstalledApps().toMutableList() if (appPreferencesRepository.shouldUpdateAppsFromOtherStores()) { - val otherStoresInstalledApps = getAppsFromOtherStores().toMutableList() + val otherStoresInstalledApps = + getAppsFromOtherStores(openSourceInstalledApps, emptyList()).toMutableList() + + val cleanApkAppsByPackage = getCleanApkDetailsByPackage(otherStoresInstalledApps) // This list is based on app signatures val updatableFDroidApps = - findPackagesMatchingFDroidSignatures(otherStoresInstalledApps) + findPackagesMatchingFDroidSignatures(otherStoresInstalledApps, cleanApkAppsByPackage) openSourceInstalledApps.addAll(updatableFDroidApps) } @@ -143,29 +159,41 @@ class UpdatesManagerImpl @Inject constructor( blockedAppRepository.isBlockedApp(it) } - if (openSourceInstalledApps.isNotEmpty()) { - status = getUpdatesFromApi({ - applicationRepository.getApplicationDetails( - openSourceInstalledApps, - Source.OPEN_SOURCE - ) - }, updateList) + val openSourceDeferred = if (openSourceInstalledApps.isNotEmpty()) { + async { + getUpdatesFromApi { + applicationRepository.getApplicationDetails( + openSourceInstalledApps, + Source.OPEN_SOURCE + ) + } + } + } else { + null + } + + val systemAppsDeferred = async { + getSystemAppUpdates() } - val systemApps = getSystemAppUpdates() + val openSourceResult = openSourceDeferred?.await() + val systemApps = systemAppsDeferred.await() + + val updateList = mutableListOf() + updateList.addAll(openSourceResult?.first.orEmpty()) val nonFaultyUpdateList = faultyAppRepository.removeFaultyApps(updateList) addSystemApps(updateList, nonFaultyUpdateList, systemApps) - return Pair(updateList, status) + val status = openSourceResult?.second ?: ResultStatus.OK + + return@coroutineScope Pair(updateList, status) } private suspend fun getSystemAppUpdates(): List { - val systemApps = mutableListOf() - getUpdatesFromApi({ + return getUpdatesFromApi { Pair(systemAppsUpdatesRepository.getSystemUpdates(), ResultStatus.OK) - }, systemApps) - return systemApps + }.first } /** @@ -229,8 +257,11 @@ class UpdatesManagerImpl @Inject constructor( * @return List of package names of apps installed from other app stores like * Aurora Store, Apk mirror, apps installed from browser, apps from ADB etc. */ - private fun getAppsFromOtherStores(): List { - val gplayAndOpenSourceInstalledApps = getGPlayInstalledApps() + getOpenSourceInstalledApps() + private fun getAppsFromOtherStores( + openSourceInstalledApps: List, + gPlayInstalledApps: List, + ): List { + val gplayAndOpenSourceInstalledApps = (gPlayInstalledApps + openSourceInstalledApps).toSet() return userApplications.filter { it.packageName !in gplayAndOpenSourceInstalledApps }.map { it.packageName } @@ -249,14 +280,12 @@ class UpdatesManagerImpl @Inject constructor( */ private suspend fun getUpdatesFromApi( apiFunction: suspend () -> Pair, ResultStatus>, - updateAccumulationList: MutableList, - ): ResultStatus { + ): Pair, ResultStatus> { val apiResult = apiFunction() val updatableApps = apiResult.first.filter { it.status == Status.UPDATABLE && (it.filterLevel.isUnFiltered() || it.isFDroidApp) } - updateAccumulationList.addAll(updatableApps) - return apiResult.second + return Pair(updatableApps, apiResult.second) } private suspend fun getGPlayUpdates( @@ -284,47 +313,54 @@ class UpdatesManagerImpl @Inject constructor( * * Map is String : String = package name : signature */ - private suspend fun getFDroidAppsAndSignatures(installedPackageNames: List): Map { + private suspend fun getFDroidAppsAndSignatures( + installedPackageNames: List, + cleanApkAppsByPackage: Map, + ): Map = coroutineScope { val appsAndSignatures = hashMapOf() - for (packageName in installedPackageNames) { - updateAppsWithPGPSignature(packageName, appsAndSignatures) + val candidates = installedPackageNames.map { packageName -> + async { + packageName to updateAppsWithPGPSignature(packageName, cleanApkAppsByPackage) + } + } + + candidates.forEach { deferred -> + val (packageName, signature) = deferred.await() + if (signature != null) { + appsAndSignatures[packageName] = signature + } } - return appsAndSignatures + return@coroutineScope appsAndSignatures } private suspend fun updateAppsWithPGPSignature( packageName: String, - appsAndSignatures: HashMap - ) { - val apps = applicationRepository.getApplicationDetails(listOf(packageName), Source.OPEN_SOURCE).first - if (apps.isEmpty()) { - return + cleanApkAppsByPackage: Map, + ): String? { + val cleanApkApplication = cleanApkAppsByPackage[packageName] + val apps = when { + cleanApkApplication != null -> listOf(cleanApkApplication) + cleanApkAppsByPackage.containsKey(packageName) -> emptyList() + else -> applicationRepository.getApplicationDetails(listOf(packageName), Source.OPEN_SOURCE).first } - - if (apps[0].package_name.isBlank()) { - return + val app = apps.firstOrNull()?.takeIf { it.package_name.isNotBlank() } + val signature = if (app == null) { + null + } else { + val pgpSignature = getPgpSignature(app) + pgpSignature } - appsAndSignatures[packageName] = getPgpSignature(apps[0]) + return signature } private suspend fun getPgpSignature(cleanApkApplication: Application): String { val installedVersionSignature = calculateSignatureVersion(cleanApkApplication) - val downloadInfoResult = handleNetworkResult { applicationRepository .getOSSDownloadInfo(cleanApkApplication._id, installedVersionSignature) .body()?.download_data } - - val pgpSignature = downloadInfoResult.data?.signature ?: "" - - Timber.i( - "Signature calculated for : ${cleanApkApplication.package_name}, " + - "signature version: $installedVersionSignature, " + - "is sig blank: ${pgpSignature.isBlank()}" - ) - - return pgpSignature + return downloadInfoResult.data?.signature ?: "" } /** @@ -336,20 +372,70 @@ class UpdatesManagerImpl @Inject constructor( */ private suspend fun findPackagesMatchingFDroidSignatures( installedPackageNames: List, - ): List { - val fDroidAppsAndSignatures = getFDroidAppsAndSignatures(installedPackageNames) + cleanApkAppsByPackage: Map, + ): List = coroutineScope { + val fDroidAppsAndSignatures = getFDroidAppsAndSignatures( + installedPackageNames, + cleanApkAppsByPackage, + ) + + val fDroidUpdatablePackageNames = mutableListOf() + val verificationTasks = fDroidAppsAndSignatures.mapNotNull { (packageName, signature) -> + if (signature.isEmpty()) { + return@mapNotNull null + } + + async { + verifyFdroidSignatureCandidate(packageName, signature) + } + } + + verificationTasks.forEach { deferred -> + val (packageName, verified) = deferred.await() + if (verified) { + fDroidUpdatablePackageNames.add(packageName) + } + } + + return@coroutineScope fDroidUpdatablePackageNames + } - val fDroidUpdatablePackageNames = fDroidAppsAndSignatures.filter { - if (it.value.isEmpty()) return@filter false + private suspend fun verifyFdroidSignatureCandidate( + packageName: String, + signature: String, + ): Pair { + val baseApkPath = appLoungePackageManager.getBaseApkPath(packageName) + if (baseApkPath.isEmpty()) { + return Pair(packageName, false) + } - // For each installed app also present on F-droid, check signature of base APK. - val baseApkPath = appLoungePackageManager.getBaseApkPath(it.key) - if (baseApkPath.isEmpty()) return@filter false + val verified = withContext(Dispatchers.IO) { + ApkSignatureManager.verifyFdroidSignature( + context, + baseApkPath, + signature, + packageName, + ) + } + return Pair(packageName, verified) + } - ApkSignatureManager.verifyFdroidSignature(context, baseApkPath, it.value, it.key) - }.map { it.key } + private suspend fun getCleanApkDetailsByPackage( + packageNames: List, + ): Map { + if (packageNames.isEmpty()) { + return emptyMap() + } - return fDroidUpdatablePackageNames + val appsResult = applicationRepository.getApplicationDetails(packageNames, Source.OPEN_SOURCE) + if (appsResult.second != ResultStatus.OK) { + return emptyMap() + } + + val appsByPackage = appsResult.first + .filter { it.package_name.isNotBlank() } + .associateBy { it.package_name } + return packageNames.associateWith { appsByPackage[it] } } /** @@ -373,13 +459,9 @@ class UpdatesManagerImpl @Inject constructor( val packageName = latestCleanapkApp.package_name val latestSignatureVersion = latestCleanapkApp.latest_downloaded_version - Timber.i("Latest signature version for $packageName : $latestSignatureVersion") - val installedVersionCode = appLoungePackageManager.getVersionCode(packageName) val installedVersionName = appLoungePackageManager.getVersionName(packageName) - Timber.i("Calculate signature for $packageName : $installedVersionCode, $installedVersionName") - val latestSignatureVersionNumber = try { latestSignatureVersion.split("_")[1].toInt() } catch (e: Exception) { @@ -396,9 +478,9 @@ class UpdatesManagerImpl @Inject constructor( it.versionCode == installedVersionCode && it.versionName == installedVersionName }?.run { builds.indexOf(this) - } ?: return "" - - Timber.i("Build info match at index: $matchingIndex") + } ?: run { + return "" + } /* If latest latest signature version is (say) "update_33" * corresponding to (say) versionCode 10, and we need to find signature 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 1d9f7cd90e8ca22486ba47b7784f03b4c80197ff..36343f717800c83a2b0e467e3a56e7203f8a57a8 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 @@ -67,6 +67,7 @@ import kotlinx.coroutines.launch import timber.log.Timber import java.util.Locale import javax.inject.Inject + @AndroidEntryPoint class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), ApplicationInstaller { @@ -83,7 +84,6 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), ApplicationI private val appProgressViewModel: AppProgressViewModel by viewModels() private var isDownloadObserverAdded = false - companion object { private const val SCROLL_TO_TOP_DELAY_MILLIS = 100L } diff --git a/app/src/main/java/foundation/e/apps/ui/updates/UpdatesViewModel.kt b/app/src/main/java/foundation/e/apps/ui/updates/UpdatesViewModel.kt index 7c86a7d5abcba750c06ca5a4de9e0be67f4e52ef..8b184308c385882fff1b4c05d0ad5b71a4c4f9d1 100644 --- a/app/src/main/java/foundation/e/apps/ui/updates/UpdatesViewModel.kt +++ b/app/src/main/java/foundation/e/apps/ui/updates/UpdatesViewModel.kt @@ -37,6 +37,7 @@ import foundation.e.apps.domain.model.User import foundation.e.apps.domain.model.install.Status import foundation.e.apps.domain.preferences.AppPreferencesRepository import foundation.e.apps.domain.preferences.SessionRepository +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import javax.inject.Inject @@ -65,7 +66,7 @@ class UpdatesViewModel @Inject constructor( return true } - fun loadUpdates() = viewModelScope.launch { + fun loadUpdates() = viewModelScope.launch(Dispatchers.IO) { exceptionsList.clear() val currentUser = sessionRepository.awaitUser() val loginState = sessionRepository.awaitLoginState()