From a8b414ead3e94cc6f5147f99733e1e1186710fef Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Mon, 13 Apr 2026 20:43:36 +0600 Subject: [PATCH 01/11] perf: reduce updates load stalls Add timing logs to attribute update latency and move update network/IO work off the main thread to keep the UI responsive. --- .../apps/data/application/apps/AppsApiImpl.kt | 24 +- .../downloadInfo/DownloadInfoApiImpl.kt | 8 +- .../e/apps/data/fdroid/FDroidRepository.kt | 6 +- .../gitlab/SystemAppsUpdatesRepository.kt | 99 +++++- .../e/apps/data/updates/UpdatesManagerImpl.kt | 304 +++++++++++++++--- .../e/apps/ui/updates/UpdatesFragment.kt | 69 ++++ .../e/apps/ui/updates/UpdatesViewModel.kt | 3 +- 7 files changed, 455 insertions(+), 58 deletions(-) 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 9b514e4e5..7f27ed3a5 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 @@ -29,6 +29,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 +40,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 +92,12 @@ 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 list = mutableListOf() + for (packageName in packageNameList) { + list.add(stores.getStore(Source.OPEN_SOURCE)?.getAppDetails(packageName) ?: Application()) + } + list } return Pair(applicationList, status) @@ -104,7 +110,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 +152,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 88e91c31c..57f500d00 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/fdroid/FDroidRepository.kt b/app/src/main/java/foundation/e/apps/data/fdroid/FDroidRepository.kt index a9821c1a8..a13c4ce99 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 9ac4c773e..ca02f773d 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 @@ -34,6 +34,7 @@ import foundation.e.apps.data.install.pkg.AppLoungePackageManager import foundation.e.apps.data.system.SystemInfoProvider import foundation.e.apps.domain.model.install.Status import timber.log.Timber +import java.util.Locale import javax.inject.Inject import javax.inject.Singleton @@ -60,6 +61,13 @@ class SystemAppsUpdatesRepository @Inject constructor( private val systemAppProjectList = mutableListOf() + private companion object { + private const val NANOS_IN_MILLI = 1_000_000L + private const val MILLIS_IN_SECOND = 1000L + private const val SECONDS_IN_MINUTE = 60L + private const val MINUTES_IN_HOUR = 60L + } + private fun getUpdatableSystemApps(): List { return systemAppProjectList.map { it.packageName } } @@ -160,17 +168,51 @@ 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 appStartMs = nowMs() + val systemAppProject = systemAppProjectList.find { it.packageName == packageName } + val releaseStartMs = nowMs() + val detailsUrl = systemAppProject?.let { + getReleaseDetailsUrl(it, releaseType) + } + Timber.tag("FAHIM").i( + "System updates release lookup, package=%s, duration=%s, urlFound=%s", + packageName, + formatDuration(nowMs() - releaseStartMs), + detailsUrl != null + ) + + val infoStartMs = nowMs() + val systemAppInfo = detailsUrl?.let { + getSystemAppInfo(packageName, it) + } + Timber.tag("FAHIM").i( + "System updates app info fetch, package=%s, duration=%s, hasInfo=%s", + packageName, + formatDuration(nowMs() - infoStartMs), + systemAppInfo != null + ) + 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 + } + + if (application != null) { + Timber.tag("FAHIM").i( + "System updates application built, package=%s, duration=%s", + packageName, + formatDuration(nowMs() - appStartMs) + ) } + + return application } private suspend fun getSystemAppInfo( @@ -231,14 +273,24 @@ class SystemAppsUpdatesRepository @Inject constructor( suspend fun getSystemUpdates(): List { val updateList = mutableListOf() + val overallStartMs = nowMs() val releaseType = getSystemReleaseType() val sdkLevel = getSdkLevel() val device = getDevice() val updatableApps = getUpdatableSystemApps() + Timber.tag("FAHIM").i( + "System updates start, totalApps=%d", + updatableApps.size + ) updatableApps.forEach { + val perAppStartMs = nowMs() if (!appLoungePackageManager.isInstalled(it)) { // Don't install for system apps which are removed (by root or otherwise) + Timber.tag("FAHIM").i( + "System updates skip (not installed), package=%s", + it + ) return@forEach } @@ -250,6 +302,12 @@ class SystemAppsUpdatesRepository @Inject constructor( device, ) } + Timber.tag("FAHIM").i( + "System updates fetch done, package=%s, duration=%s, success=%s", + it, + formatDuration(nowMs() - perAppStartMs), + result.isSuccess() + ) if (!result.isSuccess()) { Timber.e("Failed to get system app info for $it - ${result.message}") @@ -267,8 +325,33 @@ class SystemAppsUpdatesRepository @Inject constructor( } } + Timber.tag("FAHIM").i( + "System updates done, updatable=%d, duration=%s", + updateList.size, + formatDuration(nowMs() - overallStartMs) + ) + return updateList } + + private fun nowMs(): Long { + return System.nanoTime() / NANOS_IN_MILLI + } + + private fun formatDuration(durationMs: Long): String { + val totalSeconds = durationMs / MILLIS_IN_SECOND + val milliseconds = durationMs % MILLIS_IN_SECOND + val seconds = totalSeconds % SECONDS_IN_MINUTE + val totalMinutes = totalSeconds / SECONDS_IN_MINUTE + val minutes = totalMinutes % MINUTES_IN_HOUR + val hours = totalMinutes / MINUTES_IN_HOUR + + return if (hours > 0) { + String.format(Locale.US, "%d:%02d:%02d.%03d", hours, minutes, seconds, milliseconds) + } else { + String.format(Locale.US, "%02d:%02d.%03d", minutes, seconds, milliseconds) + } + } } private class UnsupportedAndroidApiException(message: String) : RuntimeException(message) 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 e4fbbdbc0..36d09847f 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 @@ -37,6 +37,7 @@ import foundation.e.apps.domain.preferences.AppPreferencesRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import timber.log.Timber +import java.util.Locale import javax.inject.Inject @Suppress("LongParameterList") @@ -54,21 +55,35 @@ class UpdatesManagerImpl @Inject constructor( const val PACKAGE_NAME_F_DROID = "org.fdroid.fdroid" const val PACKAGE_NAME_F_DROID_PRIVILEGED = "org.fdroid.fdroid.privileged" const val PACKAGE_NAME_ANDROID_VENDING = "com.android.vending" + + private const val NANOS_IN_MILLI = 1_000_000L + private const val MILLIS_IN_SECOND = 1000L + private const val SECONDS_IN_MINUTE = 60L + private const val MINUTES_IN_HOUR = 60L } private val userApplications: List get() = appLoungePackageManager.getAllUserApps() + @Suppress("LongMethod") suspend fun getUpdates(): Pair, ResultStatus> { + val overallStartMs = nowMs() val updateList = mutableListOf() var status = ResultStatus.OK - val openSourceInstalledApps = getOpenSourceInstalledApps().toMutableList() - val gPlayInstalledApps = getGPlayInstalledApps().toMutableList() + val openSourceInstalledApps = logInstalledApps("open-source") { + getOpenSourceInstalledApps() + }.toMutableList() + + val gPlayInstalledApps = logInstalledApps("Play Store") { + getGPlayInstalledApps() + }.toMutableList() if (appPreferencesRepository.shouldUpdateAppsFromOtherStores()) { withContext(Dispatchers.IO) { - val otherStoresInstalledApps = getAppsFromOtherStores().toMutableList() + val otherStoresInstalledApps = logInstalledApps("other-store") { + getAppsFromOtherStores() + }.toMutableList() // This list is based on app signatures val updatableFDroidApps = @@ -91,23 +106,27 @@ class UpdatesManagerImpl @Inject constructor( // Get open source app updates if (openSourceInstalledApps.isNotEmpty()) { - status = getUpdatesFromApi({ - applicationRepository.getApplicationDetails( - openSourceInstalledApps, - Source.OPEN_SOURCE - ) - }, updateList) + status = logUpdatesFetch("open-source", openSourceInstalledApps.size) { + getUpdatesFromApi({ + applicationRepository.getApplicationDetails( + openSourceInstalledApps, + Source.OPEN_SOURCE + ) + }, updateList) + } } // Get GPlay app updates if (getApplicationCategoryPreference().contains(ApplicationRepository.APP_TYPE_ANY) && gPlayInstalledApps.isNotEmpty() ) { - val gplayStatus = getUpdatesFromApi({ - getGPlayUpdates( - gPlayInstalledApps - ) - }, updateList) + val gplayStatus = logUpdatesFetch("Play Store", gPlayInstalledApps.size) { + getUpdatesFromApi({ + getGPlayUpdates( + gPlayInstalledApps + ) + }, updateList) + } /** If any one of the sources is successful, status should be [ResultStatus.OK] @@ -115,22 +134,35 @@ class UpdatesManagerImpl @Inject constructor( status = if (status == ResultStatus.OK) status else gplayStatus } - val systemApps = getSystemAppUpdates() + val systemApps = logSystemAppsFetch("system apps") { + getSystemAppUpdates() + } val nonFaultyUpdateList = faultyAppRepository.removeFaultyApps(updateList) addSystemApps(updateList, nonFaultyUpdateList, systemApps) + Timber.tag("FAHIM").i( + "Updates total fetch done, totalApps=%d, duration=%s", + updateList.size, + formatDuration(nowMs() - overallStartMs) + ) + return Pair(updateList, status) } suspend fun getUpdatesOSS(): Pair, ResultStatus> { + val overallStartMs = nowMs() val updateList = mutableListOf() var status = ResultStatus.OK - val openSourceInstalledApps = getOpenSourceInstalledApps().toMutableList() + val openSourceInstalledApps = logInstalledApps("OSS open-source") { + getOpenSourceInstalledApps() + }.toMutableList() if (appPreferencesRepository.shouldUpdateAppsFromOtherStores()) { - val otherStoresInstalledApps = getAppsFromOtherStores().toMutableList() + val otherStoresInstalledApps = logInstalledApps("OSS other-store") { + getAppsFromOtherStores() + }.toMutableList() // This list is based on app signatures val updatableFDroidApps = @@ -144,19 +176,29 @@ class UpdatesManagerImpl @Inject constructor( } if (openSourceInstalledApps.isNotEmpty()) { - status = getUpdatesFromApi({ - applicationRepository.getApplicationDetails( - openSourceInstalledApps, - Source.OPEN_SOURCE - ) - }, updateList) + status = logUpdatesFetch("OSS open-source", openSourceInstalledApps.size) { + getUpdatesFromApi({ + applicationRepository.getApplicationDetails( + openSourceInstalledApps, + Source.OPEN_SOURCE + ) + }, updateList) + } } - val systemApps = getSystemAppUpdates() + val systemApps = logSystemAppsFetch("OSS system apps") { + getSystemAppUpdates() + } val nonFaultyUpdateList = faultyAppRepository.removeFaultyApps(updateList) addSystemApps(updateList, nonFaultyUpdateList, systemApps) + Timber.tag("FAHIM").i( + "Updates OSS total fetch done, totalApps=%d, duration=%s", + updateList.size, + formatDuration(nowMs() - overallStartMs) + ) + return Pair(updateList, status) } @@ -285,10 +327,28 @@ class UpdatesManagerImpl @Inject constructor( * Map is String : String = package name : signature */ private suspend fun getFDroidAppsAndSignatures(installedPackageNames: List): Map { + val startMs = nowMs() + Timber.tag("FAHIM").i( + "Updates signature scan start, packages=%d", + installedPackageNames.size + ) val appsAndSignatures = hashMapOf() for (packageName in installedPackageNames) { + val perPackageStartMs = nowMs() + val beforeCount = appsAndSignatures.size updateAppsWithPGPSignature(packageName, appsAndSignatures) + Timber.tag("FAHIM").i( + "Updates signature candidate done, package=%s, added=%s, duration=%s", + packageName, + appsAndSignatures.size > beforeCount, + formatDuration(nowMs() - perPackageStartMs) + ) } + Timber.tag("FAHIM").i( + "Updates signature scan done, candidates=%d, duration=%s", + appsAndSignatures.size, + formatDuration(nowMs() - startMs) + ) return appsAndSignatures } @@ -296,7 +356,14 @@ class UpdatesManagerImpl @Inject constructor( packageName: String, appsAndSignatures: HashMap ) { + val appDetailsStartMs = nowMs() val apps = applicationRepository.getApplicationDetails(listOf(packageName), Source.OPEN_SOURCE).first + Timber.tag("FAHIM").i( + "Updates signature app details done, package=%s, duration=%s, resultCount=%d", + packageName, + formatDuration(nowMs() - appDetailsStartMs), + apps.size + ) if (apps.isEmpty()) { return } @@ -304,24 +371,48 @@ class UpdatesManagerImpl @Inject constructor( if (apps[0].package_name.isBlank()) { return } - appsAndSignatures[packageName] = getPgpSignature(apps[0]) + val signatureStartMs = nowMs() + val signature = getPgpSignature(apps[0]) + Timber.tag("FAHIM").i( + "Updates signature fetch done, package=%s, duration=%s, blank=%s", + packageName, + formatDuration(nowMs() - signatureStartMs), + signature.isBlank() + ) + appsAndSignatures[packageName] = signature } private suspend fun getPgpSignature(cleanApkApplication: Application): String { + val signatureStartMs = nowMs() + val signatureVersionStartMs = nowMs() val installedVersionSignature = calculateSignatureVersion(cleanApkApplication) + Timber.tag("FAHIM").i( + "Updates signature version done, package=%s, duration=%s, version=%s", + cleanApkApplication.package_name, + formatDuration(nowMs() - signatureVersionStartMs), + installedVersionSignature + ) + val downloadInfoStartMs = nowMs() val downloadInfoResult = handleNetworkResult { applicationRepository .getOSSDownloadInfo(cleanApkApplication._id, installedVersionSignature) .body()?.download_data } + Timber.tag("FAHIM").i( + "Updates signature download info done, package=%s, duration=%s, hasData=%s", + cleanApkApplication.package_name, + formatDuration(nowMs() - downloadInfoStartMs), + downloadInfoResult.data != null + ) val pgpSignature = downloadInfoResult.data?.signature ?: "" - Timber.i( - "Signature calculated for : ${cleanApkApplication.package_name}, " + - "signature version: $installedVersionSignature, " + - "is sig blank: ${pgpSignature.isBlank()}" + Timber.tag("FAHIM").i( + "Updates PGP signature done, package=%s, duration=%s, blank=%s", + cleanApkApplication.package_name, + formatDuration(nowMs() - signatureStartMs), + pgpSignature.isBlank() ) return pgpSignature @@ -337,17 +428,60 @@ class UpdatesManagerImpl @Inject constructor( private suspend fun findPackagesMatchingFDroidSignatures( installedPackageNames: List, ): List { + val signatureStartMs = nowMs() val fDroidAppsAndSignatures = getFDroidAppsAndSignatures(installedPackageNames) - val fDroidUpdatablePackageNames = fDroidAppsAndSignatures.filter { - if (it.value.isEmpty()) return@filter false + val fDroidUpdatablePackageNames = mutableListOf() + fDroidAppsAndSignatures.forEach { (packageName, signature) -> + if (signature.isEmpty()) { + Timber.tag("FAHIM").i( + "Updates signature skipped, package=%s, reason=empty_signature", + packageName + ) + return@forEach + } // 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 baseApkStartMs = nowMs() + val baseApkPath = appLoungePackageManager.getBaseApkPath(packageName) + val baseApkDuration = formatDuration(nowMs() - baseApkStartMs) + Timber.tag("FAHIM").i( + "Updates base APK path fetched, package=%s, duration=%s, hasPath=%s", + packageName, + baseApkDuration, + baseApkPath.isNotEmpty() + ) + if (baseApkPath.isEmpty()) { + return@forEach + } - ApkSignatureManager.verifyFdroidSignature(context, baseApkPath, it.value, it.key) - }.map { it.key } + val verifyStartMs = nowMs() + val verified = withContext(Dispatchers.IO) { + ApkSignatureManager.verifyFdroidSignature( + context, + baseApkPath, + signature, + packageName, + ) + } + Timber.tag("FAHIM").i( + "Updates signature verified, package=%s, duration=%s, matched=%s", + packageName, + formatDuration(nowMs() - verifyStartMs), + verified + ) + if (verified) { + fDroidUpdatablePackageNames.add(packageName) + } + } + + Timber.tag("FAHIM").i( + "Updates signature matching done, input=%d, candidates=%d, matched=%d, duration=%s", + installedPackageNames.size, + fDroidAppsAndSignatures.size, + fDroidUpdatablePackageNames.size, + formatDuration(nowMs() - signatureStartMs) + ) return fDroidUpdatablePackageNames } @@ -373,12 +507,21 @@ class UpdatesManagerImpl @Inject constructor( val packageName = latestCleanapkApp.package_name val latestSignatureVersion = latestCleanapkApp.latest_downloaded_version - Timber.i("Latest signature version for $packageName : $latestSignatureVersion") + Timber.tag("FAHIM").i( + "Updates signature latest version, package=%s, version=%s", + packageName, + latestSignatureVersion + ) val installedVersionCode = appLoungePackageManager.getVersionCode(packageName) val installedVersionName = appLoungePackageManager.getVersionName(packageName) - Timber.i("Calculate signature for $packageName : $installedVersionCode, $installedVersionName") + Timber.tag("FAHIM").i( + "Updates signature installed version, package=%s, code=%s, name=%s", + packageName, + installedVersionCode, + installedVersionName + ) val latestSignatureVersionNumber = try { latestSignatureVersion.split("_")[1].toInt() @@ -388,17 +531,34 @@ class UpdatesManagerImpl @Inject constructor( // Received list has build info of the latest version at the bottom. // We want it at the top. + val buildInfoStartMs = nowMs() val builds = handleNetworkResult { fDroidRepository.getBuildVersionInfo(packageName).asReversed() }.data + Timber.tag("FAHIM").i( + "Updates build info fetched, package=%s, duration=%s, buildCount=%d", + packageName, + formatDuration(nowMs() - buildInfoStartMs), + builds?.size ?: 0 + ) val matchingIndex = builds?.find { it.versionCode == installedVersionCode && it.versionName == installedVersionName }?.run { builds.indexOf(this) - } ?: return "" + } ?: run { + Timber.tag("FAHIM").i( + "Updates build info match missing, package=%s", + packageName + ) + return "" + } - Timber.i("Build info match at index: $matchingIndex") + Timber.tag("FAHIM").i( + "Updates build info match found, package=%s, index=%d", + packageName, + matchingIndex + ) /* If latest latest signature version is (say) "update_33" * corresponding to (say) versionCode 10, and we need to find signature @@ -411,4 +571,70 @@ class UpdatesManagerImpl @Inject constructor( fun getApplicationCategoryPreference(): List { return applicationRepository.getSelectedAppTypes() } + + private inline fun logInstalledApps( + label: String, + fetch: () -> List, + ): List { + val startMs = nowMs() + val apps = fetch() + Timber.tag("FAHIM").i( + "Updates %s installed apps, count=%d, duration=%s", + label, + apps.size, + formatDuration(nowMs() - startMs) + ) + return apps + } + + private suspend fun logUpdatesFetch( + label: String, + appCount: Int, + fetch: suspend () -> ResultStatus, + ): ResultStatus { + val startMs = nowMs() + val status = fetch() + Timber.tag("FAHIM").i( + "Updates %s fetch done, apps=%d, duration=%s, status=%s", + label, + appCount, + formatDuration(nowMs() - startMs), + status + ) + return status + } + + private suspend fun logSystemAppsFetch( + label: String, + fetch: suspend () -> List, + ): List { + val startMs = nowMs() + val apps = fetch() + Timber.tag("FAHIM").i( + "Updates %s fetch done, apps=%d, duration=%s", + label, + apps.size, + formatDuration(nowMs() - startMs) + ) + return apps + } + + private fun nowMs(): Long { + return System.nanoTime() / NANOS_IN_MILLI + } + + private fun formatDuration(durationMs: Long): String { + val totalSeconds = durationMs / MILLIS_IN_SECOND + val milliseconds = durationMs % MILLIS_IN_SECOND + val seconds = totalSeconds % SECONDS_IN_MINUTE + val totalMinutes = totalSeconds / SECONDS_IN_MINUTE + val minutes = totalMinutes % MINUTES_IN_HOUR + val hours = totalMinutes / MINUTES_IN_HOUR + + return if (hours > 0) { + String.format(Locale.US, "%d:%02d:%02d.%03d", hours, minutes, seconds, milliseconds) + } else { + String.format(Locale.US, "%02d:%02d.%03d", minutes, seconds, milliseconds) + } + } } 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 1d9f7cd90..8b3cbd4cf 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 @@ -19,8 +19,10 @@ package foundation.e.apps.ui.updates import android.os.Bundle +import android.os.SystemClock import android.view.View import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle @@ -67,6 +69,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,15 +86,26 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), ApplicationI private val appProgressViewModel: AppProgressViewModel by viewModels() private var isDownloadObserverAdded = false + private var updatesUiStartMs: Long = 0L + private var updatesLoadStartMs: Long = 0L companion object { private const val SCROLL_TO_TOP_DELAY_MILLIS = 100L + private const val MILLIS_IN_SECOND = 1000L + private const val SECONDS_IN_MINUTE = 60L + private const val MINUTES_IN_HOUR = 60L } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) _binding = FragmentUpdatesBinding.bind(view) + updatesUiStartMs = SystemClock.elapsedRealtime() + Timber.tag("FAHIM").i( + "Updates UI created, sinceUi=%s", + formatDuration(0L) + ) + binding.button.isEnabled = false setupListening() @@ -143,6 +157,16 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), ApplicationI // Put system apps on top val appsToDisplay = appsUpdateList.sortedByDescending { it.isSystemApp } + val now = SystemClock.elapsedRealtime() + val sinceUi = now - updatesUiStartMs + val sinceLoad = if (updatesLoadStartMs == 0L) -1L else now - updatesLoadStartMs + Timber.tag("FAHIM").i( + "Updates list received, items=%d, sinceUi=%s, sinceLoad=%s", + appsToDisplay.size, + formatDuration(sinceUi), + formatDuration(sinceLoad) + ) + listAdapter?.setData(appsToDisplay) if (!isDownloadObserverAdded) { updateButtonAvailability() @@ -152,6 +176,18 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), ApplicationI stopLoadingUI() + val afterStop = SystemClock.elapsedRealtime() + val afterStopSinceUi = afterStop - updatesUiStartMs + val afterStopSinceLoad = + if (updatesLoadStartMs == 0L) -1L else afterStop - updatesLoadStartMs + Timber.tag("FAHIM").i( + "Updates UI ready, listVisible=%s, emptyVisible=%s, sinceUi=%s, sinceLoad=%s", + binding.recyclerView.isVisible, + binding.noUpdates.isVisible, + formatDuration(afterStopSinceUi), + formatDuration(afterStopSinceLoad) + ) + binding.recyclerView.postDelayed( { _binding?.recyclerView?.scrollToPosition(0) }, SCROLL_TO_TOP_DELAY_MILLIS @@ -317,6 +353,11 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), ApplicationI override fun loadData(authObjectList: List) { if (updatesViewModel.haveSourcesChanged()) { showLoadingUI() + updatesLoadStartMs = SystemClock.elapsedRealtime() + Timber.tag("FAHIM").i( + "Updates load started, sinceUi=%s", + formatDuration(updatesLoadStartMs - updatesUiStartMs) + ) updatesViewModel.loadUpdates() initUpdateAllButton() updateButtonAvailability() @@ -335,6 +376,34 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), ApplicationI binding.noUpdates.visibility = View.GONE binding.progressBar.visibility = View.VISIBLE binding.recyclerView.visibility = View.INVISIBLE + + val now = SystemClock.elapsedRealtime() + val sinceUi = now - updatesUiStartMs + val sinceLoad = if (updatesLoadStartMs == 0L) -1L else now - updatesLoadStartMs + Timber.tag("FAHIM").i( + "Updates loading UI shown, sinceUi=%s, sinceLoad=%s", + formatDuration(sinceUi), + formatDuration(sinceLoad) + ) + } + + private fun formatDuration(durationMs: Long): String { + if (durationMs < 0L) { + return "n/a" + } + + val totalSeconds = durationMs / MILLIS_IN_SECOND + val milliseconds = durationMs % MILLIS_IN_SECOND + val seconds = totalSeconds % SECONDS_IN_MINUTE + val totalMinutes = totalSeconds / SECONDS_IN_MINUTE + val minutes = totalMinutes % MINUTES_IN_HOUR + val hours = totalMinutes / MINUTES_IN_HOUR + + return if (hours > 0) { + String.format(Locale.US, "%d:%02d:%02d.%03d", hours, minutes, seconds, milliseconds) + } else { + String.format(Locale.US, "%02d:%02d.%03d", minutes, seconds, milliseconds) + } } override fun stopLoadingUI() { 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 7c86a7d5a..8b184308c 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() -- GitLab From abe54c236ab89e0afbc68ce03bb3873d6dcd421a Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Mon, 13 Apr 2026 20:55:56 +0600 Subject: [PATCH 02/11] perf: cut update detail round trips Batch CleanAPK package lookups and reuse fetched details during signature checks to reduce repeated network calls and installer scans. --- .../apps/data/application/apps/AppsApiImpl.kt | 14 +++-- .../repositories/CleanApkAppsRepository.kt | 33 ++++++++++ .../repositories/CleanApkPwaRepository.kt | 38 ++++++++++++ .../repositories/CleanApkRepository.kt | 5 ++ .../e/apps/data/updates/UpdatesManagerImpl.kt | 60 +++++++++++++++---- 5 files changed, 135 insertions(+), 15 deletions(-) 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 7f27ed3a5..0bc095990 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 @@ -93,11 +94,16 @@ class AppsApiImpl @Inject constructor( ): Pair, ResultStatus> { val status = ResultStatus.OK val applicationList = withContext(Dispatchers.IO) { - val list = mutableListOf() - for (packageName in packageNameList) { - list.add(stores.getStore(Source.OPEN_SOURCE)?.getAppDetails(packageName) ?: Application()) + 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 } - list } return Pair(applicationList, status) 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 0dc547416..0ae24e744 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 @@ -106,6 +106,39 @@ class CleanApkAppsRepository @Inject constructor( } ?: Application() } + override suspend fun getAppDetailsForPackages(packageNames: List): List { + if (packageNames.isEmpty()) { + return emptyList() + } + + val response = cleanApkRetrofit.checkAvailablePackages( + packages = packageNames, + architectures = SystemInfoProvider.getSupportedArchitectureList() + ) + val search = response.body() + if (!response.isSuccessful || search?.success != true) { + return packageNames.map { getAppDetails(it) } + } + + val apps = search.apps + val appsByPackage = apps.associateBy { it.package_name } + + val details = mutableListOf() + for (packageName in packageNames) { + val app = appsByPackage[packageName] ?: continue + val response = cleanApkRetrofit.getAppOrPWADetailsByID( + id = app._id, + architectures = SystemInfoProvider.getSupportedArchitectureList(), + type = null + ) + response.body()?.app?.let { + details.add(it.copy(source = if (it.is_pwa) Source.PWA else Source.OPEN_SOURCE)) + } + } + + return details + } + 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 1cedff472..679766403 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 @@ -100,6 +100,44 @@ class CleanApkPwaRepository @Inject constructor( } ?: Application() } + override suspend fun getAppDetailsForPackages(packageNames: List): List { + if (packageNames.isEmpty()) { + return emptyList() + } + + val response = cleanApkRetrofit.checkAvailablePackages( + packageNames, + CleanApkRetrofit.APP_TYPE_PWA + ) + val search = response.body() + if (!response.isSuccessful || search?.success != true) { + return packageNames.map { getAppDetails(it) } + } + + val apps = search.apps + val appsByPackage = apps.associateBy { it.package_name } + + val details = mutableListOf() + for (packageName in packageNames) { + val app = appsByPackage[packageName] ?: continue + val response = cleanApkRetrofit.getAppOrPWADetailsByID(app._id, null, null) + response.body()?.app?.let { + details.add( + if (it.is_pwa) { + it.copy( + source = Source.PWA, + type = Type.PWA + ) + } else { + it + } + ) + } + } + + return details + } + 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 3d0c19e21..76fc601c7 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,6 +19,7 @@ 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 retrofit2.Response @@ -30,4 +31,8 @@ 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 packageNames.map { getAppDetails(it) } + } } 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 36d09847f..69f5b904e 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 @@ -82,12 +82,14 @@ class UpdatesManagerImpl @Inject constructor( if (appPreferencesRepository.shouldUpdateAppsFromOtherStores()) { withContext(Dispatchers.IO) { val otherStoresInstalledApps = logInstalledApps("other-store") { - getAppsFromOtherStores() + 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) @@ -161,12 +163,14 @@ class UpdatesManagerImpl @Inject constructor( if (appPreferencesRepository.shouldUpdateAppsFromOtherStores()) { val otherStoresInstalledApps = logInstalledApps("OSS other-store") { - getAppsFromOtherStores() + 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) } @@ -271,8 +275,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 } @@ -326,7 +333,10 @@ 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 { val startMs = nowMs() Timber.tag("FAHIM").i( "Updates signature scan start, packages=%d", @@ -336,7 +346,7 @@ class UpdatesManagerImpl @Inject constructor( for (packageName in installedPackageNames) { val perPackageStartMs = nowMs() val beforeCount = appsAndSignatures.size - updateAppsWithPGPSignature(packageName, appsAndSignatures) + updateAppsWithPGPSignature(packageName, appsAndSignatures, cleanApkAppsByPackage) Timber.tag("FAHIM").i( "Updates signature candidate done, package=%s, added=%s, duration=%s", packageName, @@ -354,10 +364,16 @@ class UpdatesManagerImpl @Inject constructor( private suspend fun updateAppsWithPGPSignature( packageName: String, - appsAndSignatures: HashMap + appsAndSignatures: HashMap, + cleanApkAppsByPackage: Map, ) { val appDetailsStartMs = nowMs() - val apps = applicationRepository.getApplicationDetails(listOf(packageName), Source.OPEN_SOURCE).first + val cleanApkApplication = cleanApkAppsByPackage[packageName] + val apps = when { + cleanApkApplication != null -> listOf(cleanApkApplication) + cleanApkAppsByPackage.containsKey(packageName) -> emptyList() + else -> applicationRepository.getApplicationDetails(listOf(packageName), Source.OPEN_SOURCE).first + } Timber.tag("FAHIM").i( "Updates signature app details done, package=%s, duration=%s, resultCount=%d", packageName, @@ -427,9 +443,13 @@ class UpdatesManagerImpl @Inject constructor( */ private suspend fun findPackagesMatchingFDroidSignatures( installedPackageNames: List, + cleanApkAppsByPackage: Map, ): List { val signatureStartMs = nowMs() - val fDroidAppsAndSignatures = getFDroidAppsAndSignatures(installedPackageNames) + val fDroidAppsAndSignatures = getFDroidAppsAndSignatures( + installedPackageNames, + cleanApkAppsByPackage, + ) val fDroidUpdatablePackageNames = mutableListOf() fDroidAppsAndSignatures.forEach { (packageName, signature) -> @@ -486,6 +506,24 @@ class UpdatesManagerImpl @Inject constructor( return fDroidUpdatablePackageNames } + private suspend fun getCleanApkDetailsByPackage( + packageNames: List, + ): Map { + if (packageNames.isEmpty()) { + return emptyMap() + } + + 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] } + } + /** * Get signature version for the installed version of the app. * A signature version is like "update_XX" where XX is a 2 digit number. -- GitLab From 78c26e013a8b4ab6e25b2fa6f4ca10d09fe43b61 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Mon, 13 Apr 2026 21:05:33 +0600 Subject: [PATCH 03/11] perf: parallelize update fetch work Run update sources concurrently and bound signature matching to reduce overall update list latency while keeping work coordinated. --- .../e/apps/data/updates/UpdatesManagerImpl.kt | 198 +++++++++++------- 1 file changed, 118 insertions(+), 80 deletions(-) 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 69f5b904e..a4e4964bc 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,6 +35,10 @@ 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.sync.Semaphore +import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.withContext import timber.log.Timber import java.util.Locale @@ -60,16 +64,16 @@ class UpdatesManagerImpl @Inject constructor( private const val MILLIS_IN_SECOND = 1000L private const val SECONDS_IN_MINUTE = 60L private const val MINUTES_IN_HOUR = 60L + + private const val SIGNATURE_CONCURRENCY = 4 } private val userApplications: List get() = appLoungePackageManager.getAllUserApps() @Suppress("LongMethod") - suspend fun getUpdates(): Pair, ResultStatus> { + suspend fun getUpdates(): Pair, ResultStatus> = coroutineScope { val overallStartMs = nowMs() - val updateList = mutableListOf() - var status = ResultStatus.OK val openSourceInstalledApps = logInstalledApps("open-source") { getOpenSourceInstalledApps() @@ -106,56 +110,68 @@ class UpdatesManagerImpl @Inject constructor( blockedAppRepository.isBlockedApp(it) } - // Get open source app updates - if (openSourceInstalledApps.isNotEmpty()) { - status = logUpdatesFetch("open-source", openSourceInstalledApps.size) { - getUpdatesFromApi({ - applicationRepository.getApplicationDetails( - openSourceInstalledApps, - Source.OPEN_SOURCE - ) - }, updateList) + val openSourceDeferred = if (openSourceInstalledApps.isNotEmpty()) { + async { + logUpdatesFetch("open-source", openSourceInstalledApps.size) { + 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 = logUpdatesFetch("Play Store", gPlayInstalledApps.size) { - getUpdatesFromApi({ - getGPlayUpdates( - gPlayInstalledApps - ) - }, updateList) + async { + logUpdatesFetch("Play Store", gPlayInstalledApps.size) { + getUpdatesFromApi { + getGPlayUpdates(gPlayInstalledApps) + } + } } - - /** - If any one of the sources is successful, status should be [ResultStatus.OK] - **/ - status = if (status == ResultStatus.OK) status else gplayStatus + } else { + null } - val systemApps = logSystemAppsFetch("system apps") { - getSystemAppUpdates() + val systemAppsDeferred = async { + logSystemAppsFetch("system apps") { + 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) + var status = openSourceResult?.second ?: ResultStatus.OK + if (gplayResult != null && status != ResultStatus.OK) { + status = gplayResult.second + } + Timber.tag("FAHIM").i( "Updates total fetch done, totalApps=%d, duration=%s", updateList.size, formatDuration(nowMs() - overallStartMs) ) - return Pair(updateList, status) + return@coroutineScope Pair(updateList, status) } - suspend fun getUpdatesOSS(): Pair, ResultStatus> { + suspend fun getUpdatesOSS(): Pair, ResultStatus> = coroutineScope { val overallStartMs = nowMs() - val updateList = mutableListOf() - var status = ResultStatus.OK val openSourceInstalledApps = logInstalledApps("OSS open-source") { getOpenSourceInstalledApps() @@ -179,39 +195,51 @@ class UpdatesManagerImpl @Inject constructor( blockedAppRepository.isBlockedApp(it) } - if (openSourceInstalledApps.isNotEmpty()) { - status = logUpdatesFetch("OSS open-source", openSourceInstalledApps.size) { - getUpdatesFromApi({ - applicationRepository.getApplicationDetails( - openSourceInstalledApps, - Source.OPEN_SOURCE - ) - }, updateList) + val openSourceDeferred = if (openSourceInstalledApps.isNotEmpty()) { + async { + logUpdatesFetch("OSS open-source", openSourceInstalledApps.size) { + getUpdatesFromApi { + applicationRepository.getApplicationDetails( + openSourceInstalledApps, + Source.OPEN_SOURCE + ) + } + } } + } else { + null } - val systemApps = logSystemAppsFetch("OSS system apps") { - getSystemAppUpdates() + val systemAppsDeferred = async { + logSystemAppsFetch("OSS system apps") { + 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) + val status = openSourceResult?.second ?: ResultStatus.OK + Timber.tag("FAHIM").i( "Updates OSS total fetch done, totalApps=%d, duration=%s", updateList.size, formatDuration(nowMs() - overallStartMs) ) - return Pair(updateList, status) + 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 } /** @@ -298,14 +326,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( @@ -336,22 +362,36 @@ class UpdatesManagerImpl @Inject constructor( private suspend fun getFDroidAppsAndSignatures( installedPackageNames: List, cleanApkAppsByPackage: Map, - ): Map { + ): Map = coroutineScope { val startMs = nowMs() Timber.tag("FAHIM").i( "Updates signature scan start, packages=%d", installedPackageNames.size ) val appsAndSignatures = hashMapOf() - for (packageName in installedPackageNames) { - val perPackageStartMs = nowMs() - val beforeCount = appsAndSignatures.size - updateAppsWithPGPSignature(packageName, appsAndSignatures, cleanApkAppsByPackage) + val semaphore = Semaphore(SIGNATURE_CONCURRENCY) + val candidates = installedPackageNames.map { packageName -> + async { + semaphore.withPermit { + val perPackageStartMs = nowMs() + val signature = updateAppsWithPGPSignature(packageName, cleanApkAppsByPackage) + val duration = formatDuration(nowMs() - perPackageStartMs) + Triple(packageName, signature, duration) + } + } + } + + candidates.forEach { deferred -> + val (packageName, signature, duration) = deferred.await() + val added = signature != null + if (signature != null) { + appsAndSignatures[packageName] = signature + } Timber.tag("FAHIM").i( "Updates signature candidate done, package=%s, added=%s, duration=%s", packageName, - appsAndSignatures.size > beforeCount, - formatDuration(nowMs() - perPackageStartMs) + added, + duration ) } Timber.tag("FAHIM").i( @@ -359,14 +399,13 @@ class UpdatesManagerImpl @Inject constructor( appsAndSignatures.size, formatDuration(nowMs() - startMs) ) - return appsAndSignatures + return@coroutineScope appsAndSignatures } private suspend fun updateAppsWithPGPSignature( packageName: String, - appsAndSignatures: HashMap, cleanApkAppsByPackage: Map, - ) { + ): String? { val appDetailsStartMs = nowMs() val cleanApkApplication = cleanApkAppsByPackage[packageName] val apps = when { @@ -380,22 +419,21 @@ class UpdatesManagerImpl @Inject constructor( formatDuration(nowMs() - appDetailsStartMs), apps.size ) - if (apps.isEmpty()) { - return - } - - if (apps[0].package_name.isBlank()) { - return + val app = apps.firstOrNull()?.takeIf { it.package_name.isNotBlank() } + val signature = if (app == null) { + null + } else { + val signatureStartMs = nowMs() + val pgpSignature = getPgpSignature(app) + Timber.tag("FAHIM").i( + "Updates signature fetch done, package=%s, duration=%s, blank=%s", + packageName, + formatDuration(nowMs() - signatureStartMs), + pgpSignature.isBlank() + ) + pgpSignature } - val signatureStartMs = nowMs() - val signature = getPgpSignature(apps[0]) - Timber.tag("FAHIM").i( - "Updates signature fetch done, package=%s, duration=%s, blank=%s", - packageName, - formatDuration(nowMs() - signatureStartMs), - signature.isBlank() - ) - appsAndSignatures[packageName] = signature + return signature } private suspend fun getPgpSignature(cleanApkApplication: Application): String { @@ -628,18 +666,18 @@ class UpdatesManagerImpl @Inject constructor( private suspend fun logUpdatesFetch( label: String, appCount: Int, - fetch: suspend () -> ResultStatus, - ): ResultStatus { + fetch: suspend () -> Pair, ResultStatus>, + ): Pair, ResultStatus> { val startMs = nowMs() - val status = fetch() + val result = fetch() Timber.tag("FAHIM").i( "Updates %s fetch done, apps=%d, duration=%s, status=%s", label, appCount, formatDuration(nowMs() - startMs), - status + result.second ) - return status + return result } private suspend fun logSystemAppsFetch( -- GitLab From 84a1faca67f3f7eeee2318da8bfcbdbf2a09dce8 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Mon, 13 Apr 2026 21:21:01 +0600 Subject: [PATCH 04/11] fix: avoid incomplete CleanAPK batch details Fall back to per-package detail fetches so list items keep the Open Source label when batch availability data is incomplete. --- .../repositories/CleanApkAppsRepository.kt | 31 +--------------- .../repositories/CleanApkPwaRepository.kt | 36 +------------------ 2 files changed, 2 insertions(+), 65 deletions(-) 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 0ae24e744..9dbb5e2b1 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 @@ -107,36 +107,7 @@ class CleanApkAppsRepository @Inject constructor( } override suspend fun getAppDetailsForPackages(packageNames: List): List { - if (packageNames.isEmpty()) { - return emptyList() - } - - val response = cleanApkRetrofit.checkAvailablePackages( - packages = packageNames, - architectures = SystemInfoProvider.getSupportedArchitectureList() - ) - val search = response.body() - if (!response.isSuccessful || search?.success != true) { - return packageNames.map { getAppDetails(it) } - } - - val apps = search.apps - val appsByPackage = apps.associateBy { it.package_name } - - val details = mutableListOf() - for (packageName in packageNames) { - val app = appsByPackage[packageName] ?: continue - val response = cleanApkRetrofit.getAppOrPWADetailsByID( - id = app._id, - architectures = SystemInfoProvider.getSupportedArchitectureList(), - type = null - ) - response.body()?.app?.let { - details.add(it.copy(source = if (it.is_pwa) Source.PWA else Source.OPEN_SOURCE)) - } - } - - return details + return packageNames.map { getAppDetails(it) } } override suspend fun getSearchResults(pattern: String): List { 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 679766403..e07a2af63 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 @@ -101,41 +101,7 @@ class CleanApkPwaRepository @Inject constructor( } override suspend fun getAppDetailsForPackages(packageNames: List): List { - if (packageNames.isEmpty()) { - return emptyList() - } - - val response = cleanApkRetrofit.checkAvailablePackages( - packageNames, - CleanApkRetrofit.APP_TYPE_PWA - ) - val search = response.body() - if (!response.isSuccessful || search?.success != true) { - return packageNames.map { getAppDetails(it) } - } - - val apps = search.apps - val appsByPackage = apps.associateBy { it.package_name } - - val details = mutableListOf() - for (packageName in packageNames) { - val app = appsByPackage[packageName] ?: continue - val response = cleanApkRetrofit.getAppOrPWADetailsByID(app._id, null, null) - response.body()?.app?.let { - details.add( - if (it.is_pwa) { - it.copy( - source = Source.PWA, - type = Type.PWA - ) - } else { - it - } - ) - } - } - - return details + return packageNames.map { getAppDetails(it) } } override suspend fun getSearchResults(pattern: String): List { -- GitLab From fb6bdf344ba5da16161c974bc80c62596d05ad67 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Mon, 13 Apr 2026 21:55:56 +0600 Subject: [PATCH 05/11] perf: drop signature concurrency cap Remove the semaphore limit so signature checks can run fully in parallel for faster matching. --- .../e/apps/data/updates/UpdatesManagerImpl.kt | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) 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 a4e4964bc..7fca60953 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 @@ -37,8 +37,6 @@ import foundation.e.apps.domain.preferences.AppPreferencesRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.withContext import timber.log.Timber import java.util.Locale @@ -64,8 +62,6 @@ class UpdatesManagerImpl @Inject constructor( private const val MILLIS_IN_SECOND = 1000L private const val SECONDS_IN_MINUTE = 60L private const val MINUTES_IN_HOUR = 60L - - private const val SIGNATURE_CONCURRENCY = 4 } private val userApplications: List @@ -369,15 +365,12 @@ class UpdatesManagerImpl @Inject constructor( installedPackageNames.size ) val appsAndSignatures = hashMapOf() - val semaphore = Semaphore(SIGNATURE_CONCURRENCY) val candidates = installedPackageNames.map { packageName -> async { - semaphore.withPermit { - val perPackageStartMs = nowMs() - val signature = updateAppsWithPGPSignature(packageName, cleanApkAppsByPackage) - val duration = formatDuration(nowMs() - perPackageStartMs) - Triple(packageName, signature, duration) - } + val perPackageStartMs = nowMs() + val signature = updateAppsWithPGPSignature(packageName, cleanApkAppsByPackage) + val duration = formatDuration(nowMs() - perPackageStartMs) + Triple(packageName, signature, duration) } } -- GitLab From d548fd32d2e7228d03e490fae21c629a139b9bde Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Mon, 13 Apr 2026 22:20:22 +0600 Subject: [PATCH 06/11] perf: parallelize CleanAPK detail fetches Run per-package detail requests concurrently to shorten update list retrieval for multiple packages. --- .../data/cleanapk/repositories/CleanApkRepository.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 76fc601c7..4fda8b90f 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 @@ -22,6 +22,9 @@ 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 @@ -33,6 +36,10 @@ interface CleanApkRepository : StoreRepository { suspend fun checkAvailablePackages(packageNames: List): Response suspend fun getAppDetailsForPackages(packageNames: List): List { - return packageNames.map { getAppDetails(it) } + return coroutineScope { + packageNames.map { + async { getAppDetails(it) } + }.awaitAll() + } } } -- GitLab From 0d3a08fecb0a7fb245de7f3d8e8ed2b5eba4a93a Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Mon, 13 Apr 2026 22:34:04 +0600 Subject: [PATCH 07/11] perf: maximize CleanAPK detail concurrency Use batch availability as a prefilter and fetch full details in parallel, with per-package fallback to keep labels correct. --- .../repositories/CleanApkAppsRepository.kt | 39 ++++++++++++++++- .../repositories/CleanApkPwaRepository.kt | 42 ++++++++++++++++++- 2 files changed, 79 insertions(+), 2 deletions(-) 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 9dbb5e2b1..edf7c40dd 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 @@ -107,7 +110,41 @@ class CleanApkAppsRepository @Inject constructor( } override suspend fun getAppDetailsForPackages(packageNames: List): List { - return packageNames.map { getAppDetails(it) } + 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 { 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 e07a2af63..5eb6ec04b 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 @@ -101,7 +104,44 @@ class CleanApkPwaRepository @Inject constructor( } override suspend fun getAppDetailsForPackages(packageNames: List): List { - return packageNames.map { getAppDetails(it) } + 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 { -- GitLab From c2a1e19f763d59b9d8adc289851a2053bef0f91c Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Mon, 13 Apr 2026 23:04:05 +0600 Subject: [PATCH 08/11] perf: cap parallel signature and system fetch Run signature verification and system app update fetches concurrently with conservative caps to reduce latency without saturating IO. --- .../gitlab/SystemAppsUpdatesRepository.kt | 91 +++++++++++++------ .../e/apps/data/updates/UpdatesManagerImpl.kt | 82 ++++++++++------- 2 files changed, 113 insertions(+), 60 deletions(-) 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 ca02f773d..d6c1601f7 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,10 @@ 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 kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit import timber.log.Timber import java.util.Locale import javax.inject.Inject @@ -66,6 +70,8 @@ class SystemAppsUpdatesRepository @Inject constructor( private const val MILLIS_IN_SECOND = 1000L private const val SECONDS_IN_MINUTE = 60L private const val MINUTES_IN_HOUR = 60L + + private const val SYSTEM_APP_CONCURRENCY = 2 } private fun getUpdatableSystemApps(): List { @@ -271,7 +277,7 @@ class SystemAppsUpdatesRepository @Inject constructor( } } - suspend fun getSystemUpdates(): List { + suspend fun getSystemUpdates(): List = coroutineScope { val updateList = mutableListOf() val overallStartMs = nowMs() val releaseType = getSystemReleaseType() @@ -283,55 +289,82 @@ class SystemAppsUpdatesRepository @Inject constructor( "System updates start, totalApps=%d", updatableApps.size ) - updatableApps.forEach { - val perAppStartMs = nowMs() - if (!appLoungePackageManager.isInstalled(it)) { - // Don't install for system apps which are removed (by root or otherwise) - Timber.tag("FAHIM").i( - "System updates skip (not installed), package=%s", - it - ) - return@forEach + + val semaphore = Semaphore(SYSTEM_APP_CONCURRENCY) + val fetchTasks = updatableApps.map { packageName -> + async { + semaphore.withPermit { + fetchSystemAppUpdate(packageName, releaseType, sdkLevel, device) + } } + } + + fetchTasks.forEach { deferred -> + deferred.await()?.let { updateList.add(it) } + } + + Timber.tag("FAHIM").i( + "System updates done, updatable=%d, duration=%s", + updateList.size, + formatDuration(nowMs() - overallStartMs) + ) + + return@coroutineScope updateList + } + + private suspend fun fetchSystemAppUpdate( + packageName: String, + releaseType: OsReleaseType, + sdkLevel: Int, + device: String, + ): Application? { + val perAppStartMs = nowMs() + val installed = appLoungePackageManager.isInstalled(packageName) + if (!installed) { + // Don't install for system apps which are removed (by root or otherwise) + Timber.tag("FAHIM").i( + "System updates skip (not installed), package=%s", + packageName + ) + } - val result = handleNetworkResult { + val result = if (installed) { + handleNetworkResult { getApplication( - it, + packageName, releaseType, sdkLevel, device, ) } + } else { + null + } + + if (result != null) { Timber.tag("FAHIM").i( "System updates fetch done, package=%s, duration=%s, success=%s", - it, + packageName, formatDuration(nowMs() - perAppStartMs), result.isSuccess() ) - 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 } - - Timber.tag("FAHIM").i( - "System updates done, updatable=%d, duration=%s", - updateList.size, - formatDuration(nowMs() - overallStartMs) - ) - - return updateList + return updateApp } private fun nowMs(): Long { 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 7fca60953..cfeaa8aaf 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 @@ -37,6 +37,8 @@ import foundation.e.apps.domain.preferences.AppPreferencesRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.sync.Semaphore +import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.withContext import timber.log.Timber import java.util.Locale @@ -62,6 +64,8 @@ class UpdatesManagerImpl @Inject constructor( private const val MILLIS_IN_SECOND = 1000L private const val SECONDS_IN_MINUTE = 60L private const val MINUTES_IN_HOUR = 60L + + private const val SIGNATURE_VERIFY_CONCURRENCY = 3 } private val userApplications: List @@ -475,7 +479,7 @@ class UpdatesManagerImpl @Inject constructor( private suspend fun findPackagesMatchingFDroidSignatures( installedPackageNames: List, cleanApkAppsByPackage: Map, - ): List { + ): List = coroutineScope { val signatureStartMs = nowMs() val fDroidAppsAndSignatures = getFDroidAppsAndSignatures( installedPackageNames, @@ -483,44 +487,25 @@ class UpdatesManagerImpl @Inject constructor( ) val fDroidUpdatablePackageNames = mutableListOf() - fDroidAppsAndSignatures.forEach { (packageName, signature) -> + val semaphore = Semaphore(SIGNATURE_VERIFY_CONCURRENCY) + val verificationTasks = fDroidAppsAndSignatures.mapNotNull { (packageName, signature) -> if (signature.isEmpty()) { Timber.tag("FAHIM").i( "Updates signature skipped, package=%s, reason=empty_signature", packageName ) - return@forEach + return@mapNotNull null } - // For each installed app also present on F-droid, check signature of base APK. - val baseApkStartMs = nowMs() - val baseApkPath = appLoungePackageManager.getBaseApkPath(packageName) - val baseApkDuration = formatDuration(nowMs() - baseApkStartMs) - Timber.tag("FAHIM").i( - "Updates base APK path fetched, package=%s, duration=%s, hasPath=%s", - packageName, - baseApkDuration, - baseApkPath.isNotEmpty() - ) - if (baseApkPath.isEmpty()) { - return@forEach + async { + semaphore.withPermit { + verifyFdroidSignatureCandidate(packageName, signature) + } } + } - val verifyStartMs = nowMs() - val verified = withContext(Dispatchers.IO) { - ApkSignatureManager.verifyFdroidSignature( - context, - baseApkPath, - signature, - packageName, - ) - } - Timber.tag("FAHIM").i( - "Updates signature verified, package=%s, duration=%s, matched=%s", - packageName, - formatDuration(nowMs() - verifyStartMs), - verified - ) + verificationTasks.forEach { deferred -> + val (packageName, verified) = deferred.await() if (verified) { fDroidUpdatablePackageNames.add(packageName) } @@ -534,7 +519,42 @@ class UpdatesManagerImpl @Inject constructor( formatDuration(nowMs() - signatureStartMs) ) - return fDroidUpdatablePackageNames + return@coroutineScope fDroidUpdatablePackageNames + } + + private suspend fun verifyFdroidSignatureCandidate( + packageName: String, + signature: String, + ): Pair { + val baseApkStartMs = nowMs() + val baseApkPath = appLoungePackageManager.getBaseApkPath(packageName) + val baseApkDuration = formatDuration(nowMs() - baseApkStartMs) + Timber.tag("FAHIM").i( + "Updates base APK path fetched, package=%s, duration=%s, hasPath=%s", + packageName, + baseApkDuration, + baseApkPath.isNotEmpty() + ) + if (baseApkPath.isEmpty()) { + return Pair(packageName, false) + } + + val verifyStartMs = nowMs() + val verified = withContext(Dispatchers.IO) { + ApkSignatureManager.verifyFdroidSignature( + context, + baseApkPath, + signature, + packageName, + ) + } + Timber.tag("FAHIM").i( + "Updates signature verified, package=%s, duration=%s, matched=%s", + packageName, + formatDuration(nowMs() - verifyStartMs), + verified + ) + return Pair(packageName, verified) } private suspend fun getCleanApkDetailsByPackage( -- GitLab From 97d18bfb00abc759012f276b609b3731b5ace1f8 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Mon, 13 Apr 2026 23:20:16 +0600 Subject: [PATCH 09/11] perf: remove system app concurrency cap Let system app update fetches run concurrently without a semaphore limit. --- .../e/apps/data/gitlab/SystemAppsUpdatesRepository.kt | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) 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 d6c1601f7..913b2f423 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 @@ -35,8 +35,6 @@ import foundation.e.apps.data.system.SystemInfoProvider import foundation.e.apps.domain.model.install.Status import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit import timber.log.Timber import java.util.Locale import javax.inject.Inject @@ -70,8 +68,6 @@ class SystemAppsUpdatesRepository @Inject constructor( private const val MILLIS_IN_SECOND = 1000L private const val SECONDS_IN_MINUTE = 60L private const val MINUTES_IN_HOUR = 60L - - private const val SYSTEM_APP_CONCURRENCY = 2 } private fun getUpdatableSystemApps(): List { @@ -290,12 +286,9 @@ class SystemAppsUpdatesRepository @Inject constructor( updatableApps.size ) - val semaphore = Semaphore(SYSTEM_APP_CONCURRENCY) val fetchTasks = updatableApps.map { packageName -> async { - semaphore.withPermit { - fetchSystemAppUpdate(packageName, releaseType, sdkLevel, device) - } + fetchSystemAppUpdate(packageName, releaseType, sdkLevel, device) } } -- GitLab From 29313baefd103b12cd58f5997aad5cf947b9f917 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Tue, 14 Apr 2026 00:07:43 +0600 Subject: [PATCH 10/11] perf: remove signature verify cap Let signature verification run fully in parallel to minimize matching time. --- .../foundation/e/apps/data/updates/UpdatesManagerImpl.kt | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) 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 cfeaa8aaf..efee7c5e4 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 @@ -37,8 +37,6 @@ import foundation.e.apps.domain.preferences.AppPreferencesRepository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withPermit import kotlinx.coroutines.withContext import timber.log.Timber import java.util.Locale @@ -64,8 +62,6 @@ class UpdatesManagerImpl @Inject constructor( private const val MILLIS_IN_SECOND = 1000L private const val SECONDS_IN_MINUTE = 60L private const val MINUTES_IN_HOUR = 60L - - private const val SIGNATURE_VERIFY_CONCURRENCY = 3 } private val userApplications: List @@ -487,7 +483,6 @@ class UpdatesManagerImpl @Inject constructor( ) val fDroidUpdatablePackageNames = mutableListOf() - val semaphore = Semaphore(SIGNATURE_VERIFY_CONCURRENCY) val verificationTasks = fDroidAppsAndSignatures.mapNotNull { (packageName, signature) -> if (signature.isEmpty()) { Timber.tag("FAHIM").i( @@ -498,9 +493,7 @@ class UpdatesManagerImpl @Inject constructor( } async { - semaphore.withPermit { - verifyFdroidSignatureCandidate(packageName, signature) - } + verifyFdroidSignatureCandidate(packageName, signature) } } -- GitLab From 834224ba556fbe0e0dcae076ffacaba55171e33f Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Tue, 14 Apr 2026 12:08:52 +0600 Subject: [PATCH 11/11] chore: remove update timing traces Baseline timing logs are no longer needed. --- .../gitlab/SystemAppsUpdatesRepository.kt | 76 ----- .../e/apps/data/updates/UpdatesManagerImpl.kt | 274 ++---------------- .../e/apps/ui/updates/UpdatesFragment.kt | 69 ----- 3 files changed, 24 insertions(+), 395 deletions(-) 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 913b2f423..59459aa76 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 @@ -36,7 +36,6 @@ import foundation.e.apps.domain.model.install.Status import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import timber.log.Timber -import java.util.Locale import javax.inject.Inject import javax.inject.Singleton @@ -63,13 +62,6 @@ class SystemAppsUpdatesRepository @Inject constructor( private val systemAppProjectList = mutableListOf() - private companion object { - private const val NANOS_IN_MILLI = 1_000_000L - private const val MILLIS_IN_SECOND = 1000L - private const val SECONDS_IN_MINUTE = 60L - private const val MINUTES_IN_HOUR = 60L - } - private fun getUpdatableSystemApps(): List { return systemAppProjectList.map { it.packageName } } @@ -170,29 +162,13 @@ class SystemAppsUpdatesRepository @Inject constructor( sdkLevel: Int, device: String, ): Application? { - val appStartMs = nowMs() val systemAppProject = systemAppProjectList.find { it.packageName == packageName } - val releaseStartMs = nowMs() val detailsUrl = systemAppProject?.let { getReleaseDetailsUrl(it, releaseType) } - Timber.tag("FAHIM").i( - "System updates release lookup, package=%s, duration=%s, urlFound=%s", - packageName, - formatDuration(nowMs() - releaseStartMs), - detailsUrl != null - ) - - val infoStartMs = nowMs() val systemAppInfo = detailsUrl?.let { getSystemAppInfo(packageName, it) } - Timber.tag("FAHIM").i( - "System updates app info fetch, package=%s, duration=%s, hasInfo=%s", - packageName, - formatDuration(nowMs() - infoStartMs), - systemAppInfo != null - ) val isBlocked = systemAppInfo?.let { isSystemAppBlocked(it, sdkLevel, device) } == true @@ -206,14 +182,6 @@ class SystemAppsUpdatesRepository @Inject constructor( null } - if (application != null) { - Timber.tag("FAHIM").i( - "System updates application built, package=%s, duration=%s", - packageName, - formatDuration(nowMs() - appStartMs) - ) - } - return application } @@ -275,16 +243,11 @@ class SystemAppsUpdatesRepository @Inject constructor( suspend fun getSystemUpdates(): List = coroutineScope { val updateList = mutableListOf() - val overallStartMs = nowMs() val releaseType = getSystemReleaseType() val sdkLevel = getSdkLevel() val device = getDevice() val updatableApps = getUpdatableSystemApps() - Timber.tag("FAHIM").i( - "System updates start, totalApps=%d", - updatableApps.size - ) val fetchTasks = updatableApps.map { packageName -> async { @@ -296,12 +259,6 @@ class SystemAppsUpdatesRepository @Inject constructor( deferred.await()?.let { updateList.add(it) } } - Timber.tag("FAHIM").i( - "System updates done, updatable=%d, duration=%s", - updateList.size, - formatDuration(nowMs() - overallStartMs) - ) - return@coroutineScope updateList } @@ -311,15 +268,7 @@ class SystemAppsUpdatesRepository @Inject constructor( sdkLevel: Int, device: String, ): Application? { - val perAppStartMs = nowMs() val installed = appLoungePackageManager.isInstalled(packageName) - if (!installed) { - // Don't install for system apps which are removed (by root or otherwise) - Timber.tag("FAHIM").i( - "System updates skip (not installed), package=%s", - packageName - ) - } val result = if (installed) { handleNetworkResult { @@ -335,12 +284,6 @@ class SystemAppsUpdatesRepository @Inject constructor( } if (result != null) { - Timber.tag("FAHIM").i( - "System updates fetch done, package=%s, duration=%s, success=%s", - packageName, - formatDuration(nowMs() - perAppStartMs), - result.isSuccess() - ) if (!result.isSuccess()) { Timber.e("Failed to get system app info for $packageName - ${result.message}") } @@ -359,25 +302,6 @@ class SystemAppsUpdatesRepository @Inject constructor( } return updateApp } - - private fun nowMs(): Long { - return System.nanoTime() / NANOS_IN_MILLI - } - - private fun formatDuration(durationMs: Long): String { - val totalSeconds = durationMs / MILLIS_IN_SECOND - val milliseconds = durationMs % MILLIS_IN_SECOND - val seconds = totalSeconds % SECONDS_IN_MINUTE - val totalMinutes = totalSeconds / SECONDS_IN_MINUTE - val minutes = totalMinutes % MINUTES_IN_HOUR - val hours = totalMinutes / MINUTES_IN_HOUR - - return if (hours > 0) { - String.format(Locale.US, "%d:%02d:%02d.%03d", hours, minutes, seconds, milliseconds) - } else { - String.format(Locale.US, "%02d:%02d.%03d", minutes, seconds, milliseconds) - } - } } private class UnsupportedAndroidApiException(message: String) : RuntimeException(message) 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 efee7c5e4..e51b6337a 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 @@ -38,8 +38,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.withContext -import timber.log.Timber -import java.util.Locale import javax.inject.Inject @Suppress("LongParameterList") @@ -57,11 +55,6 @@ class UpdatesManagerImpl @Inject constructor( const val PACKAGE_NAME_F_DROID = "org.fdroid.fdroid" const val PACKAGE_NAME_F_DROID_PRIVILEGED = "org.fdroid.fdroid.privileged" const val PACKAGE_NAME_ANDROID_VENDING = "com.android.vending" - - private const val NANOS_IN_MILLI = 1_000_000L - private const val MILLIS_IN_SECOND = 1000L - private const val SECONDS_IN_MINUTE = 60L - private const val MINUTES_IN_HOUR = 60L } private val userApplications: List @@ -69,21 +62,13 @@ class UpdatesManagerImpl @Inject constructor( @Suppress("LongMethod") suspend fun getUpdates(): Pair, ResultStatus> = coroutineScope { - val overallStartMs = nowMs() - - val openSourceInstalledApps = logInstalledApps("open-source") { - getOpenSourceInstalledApps() - }.toMutableList() - - val gPlayInstalledApps = logInstalledApps("Play Store") { - getGPlayInstalledApps() - }.toMutableList() + val openSourceInstalledApps = getOpenSourceInstalledApps().toMutableList() + val gPlayInstalledApps = getGPlayInstalledApps().toMutableList() if (appPreferencesRepository.shouldUpdateAppsFromOtherStores()) { withContext(Dispatchers.IO) { - val otherStoresInstalledApps = logInstalledApps("other-store") { - getAppsFromOtherStores(openSourceInstalledApps, gPlayInstalledApps) - }.toMutableList() + val otherStoresInstalledApps = + getAppsFromOtherStores(openSourceInstalledApps, gPlayInstalledApps).toMutableList() val cleanApkAppsByPackage = getCleanApkDetailsByPackage(otherStoresInstalledApps) @@ -108,13 +93,11 @@ class UpdatesManagerImpl @Inject constructor( val openSourceDeferred = if (openSourceInstalledApps.isNotEmpty()) { async { - logUpdatesFetch("open-source", openSourceInstalledApps.size) { - getUpdatesFromApi { - applicationRepository.getApplicationDetails( - openSourceInstalledApps, - Source.OPEN_SOURCE - ) - } + getUpdatesFromApi { + applicationRepository.getApplicationDetails( + openSourceInstalledApps, + Source.OPEN_SOURCE + ) } } } else { @@ -125,10 +108,8 @@ class UpdatesManagerImpl @Inject constructor( gPlayInstalledApps.isNotEmpty() ) { async { - logUpdatesFetch("Play Store", gPlayInstalledApps.size) { - getUpdatesFromApi { - getGPlayUpdates(gPlayInstalledApps) - } + getUpdatesFromApi { + getGPlayUpdates(gPlayInstalledApps) } } } else { @@ -136,9 +117,7 @@ class UpdatesManagerImpl @Inject constructor( } val systemAppsDeferred = async { - logSystemAppsFetch("system apps") { - getSystemAppUpdates() - } + getSystemAppUpdates() } val openSourceResult = openSourceDeferred?.await() @@ -157,26 +136,15 @@ class UpdatesManagerImpl @Inject constructor( status = gplayResult.second } - Timber.tag("FAHIM").i( - "Updates total fetch done, totalApps=%d, duration=%s", - updateList.size, - formatDuration(nowMs() - overallStartMs) - ) - return@coroutineScope Pair(updateList, status) } suspend fun getUpdatesOSS(): Pair, ResultStatus> = coroutineScope { - val overallStartMs = nowMs() - - val openSourceInstalledApps = logInstalledApps("OSS open-source") { - getOpenSourceInstalledApps() - }.toMutableList() + val openSourceInstalledApps = getOpenSourceInstalledApps().toMutableList() if (appPreferencesRepository.shouldUpdateAppsFromOtherStores()) { - val otherStoresInstalledApps = logInstalledApps("OSS other-store") { - getAppsFromOtherStores(openSourceInstalledApps, emptyList()) - }.toMutableList() + val otherStoresInstalledApps = + getAppsFromOtherStores(openSourceInstalledApps, emptyList()).toMutableList() val cleanApkAppsByPackage = getCleanApkDetailsByPackage(otherStoresInstalledApps) @@ -193,13 +161,11 @@ class UpdatesManagerImpl @Inject constructor( val openSourceDeferred = if (openSourceInstalledApps.isNotEmpty()) { async { - logUpdatesFetch("OSS open-source", openSourceInstalledApps.size) { - getUpdatesFromApi { - applicationRepository.getApplicationDetails( - openSourceInstalledApps, - Source.OPEN_SOURCE - ) - } + getUpdatesFromApi { + applicationRepository.getApplicationDetails( + openSourceInstalledApps, + Source.OPEN_SOURCE + ) } } } else { @@ -207,9 +173,7 @@ class UpdatesManagerImpl @Inject constructor( } val systemAppsDeferred = async { - logSystemAppsFetch("OSS system apps") { - getSystemAppUpdates() - } + getSystemAppUpdates() } val openSourceResult = openSourceDeferred?.await() @@ -223,12 +187,6 @@ class UpdatesManagerImpl @Inject constructor( val status = openSourceResult?.second ?: ResultStatus.OK - Timber.tag("FAHIM").i( - "Updates OSS total fetch done, totalApps=%d, duration=%s", - updateList.size, - formatDuration(nowMs() - overallStartMs) - ) - return@coroutineScope Pair(updateList, status) } @@ -359,39 +317,19 @@ class UpdatesManagerImpl @Inject constructor( installedPackageNames: List, cleanApkAppsByPackage: Map, ): Map = coroutineScope { - val startMs = nowMs() - Timber.tag("FAHIM").i( - "Updates signature scan start, packages=%d", - installedPackageNames.size - ) val appsAndSignatures = hashMapOf() val candidates = installedPackageNames.map { packageName -> async { - val perPackageStartMs = nowMs() - val signature = updateAppsWithPGPSignature(packageName, cleanApkAppsByPackage) - val duration = formatDuration(nowMs() - perPackageStartMs) - Triple(packageName, signature, duration) + packageName to updateAppsWithPGPSignature(packageName, cleanApkAppsByPackage) } } candidates.forEach { deferred -> - val (packageName, signature, duration) = deferred.await() - val added = signature != null + val (packageName, signature) = deferred.await() if (signature != null) { appsAndSignatures[packageName] = signature } - Timber.tag("FAHIM").i( - "Updates signature candidate done, package=%s, added=%s, duration=%s", - packageName, - added, - duration - ) } - Timber.tag("FAHIM").i( - "Updates signature scan done, candidates=%d, duration=%s", - appsAndSignatures.size, - formatDuration(nowMs() - startMs) - ) return@coroutineScope appsAndSignatures } @@ -399,70 +337,30 @@ class UpdatesManagerImpl @Inject constructor( packageName: String, cleanApkAppsByPackage: Map, ): String? { - val appDetailsStartMs = nowMs() val cleanApkApplication = cleanApkAppsByPackage[packageName] val apps = when { cleanApkApplication != null -> listOf(cleanApkApplication) cleanApkAppsByPackage.containsKey(packageName) -> emptyList() else -> applicationRepository.getApplicationDetails(listOf(packageName), Source.OPEN_SOURCE).first } - Timber.tag("FAHIM").i( - "Updates signature app details done, package=%s, duration=%s, resultCount=%d", - packageName, - formatDuration(nowMs() - appDetailsStartMs), - apps.size - ) val app = apps.firstOrNull()?.takeIf { it.package_name.isNotBlank() } val signature = if (app == null) { null } else { - val signatureStartMs = nowMs() val pgpSignature = getPgpSignature(app) - Timber.tag("FAHIM").i( - "Updates signature fetch done, package=%s, duration=%s, blank=%s", - packageName, - formatDuration(nowMs() - signatureStartMs), - pgpSignature.isBlank() - ) pgpSignature } return signature } private suspend fun getPgpSignature(cleanApkApplication: Application): String { - val signatureStartMs = nowMs() - val signatureVersionStartMs = nowMs() val installedVersionSignature = calculateSignatureVersion(cleanApkApplication) - Timber.tag("FAHIM").i( - "Updates signature version done, package=%s, duration=%s, version=%s", - cleanApkApplication.package_name, - formatDuration(nowMs() - signatureVersionStartMs), - installedVersionSignature - ) - - val downloadInfoStartMs = nowMs() val downloadInfoResult = handleNetworkResult { applicationRepository .getOSSDownloadInfo(cleanApkApplication._id, installedVersionSignature) .body()?.download_data } - Timber.tag("FAHIM").i( - "Updates signature download info done, package=%s, duration=%s, hasData=%s", - cleanApkApplication.package_name, - formatDuration(nowMs() - downloadInfoStartMs), - downloadInfoResult.data != null - ) - - val pgpSignature = downloadInfoResult.data?.signature ?: "" - - Timber.tag("FAHIM").i( - "Updates PGP signature done, package=%s, duration=%s, blank=%s", - cleanApkApplication.package_name, - formatDuration(nowMs() - signatureStartMs), - pgpSignature.isBlank() - ) - - return pgpSignature + return downloadInfoResult.data?.signature ?: "" } /** @@ -476,7 +374,6 @@ class UpdatesManagerImpl @Inject constructor( installedPackageNames: List, cleanApkAppsByPackage: Map, ): List = coroutineScope { - val signatureStartMs = nowMs() val fDroidAppsAndSignatures = getFDroidAppsAndSignatures( installedPackageNames, cleanApkAppsByPackage, @@ -485,10 +382,6 @@ class UpdatesManagerImpl @Inject constructor( val fDroidUpdatablePackageNames = mutableListOf() val verificationTasks = fDroidAppsAndSignatures.mapNotNull { (packageName, signature) -> if (signature.isEmpty()) { - Timber.tag("FAHIM").i( - "Updates signature skipped, package=%s, reason=empty_signature", - packageName - ) return@mapNotNull null } @@ -504,14 +397,6 @@ class UpdatesManagerImpl @Inject constructor( } } - Timber.tag("FAHIM").i( - "Updates signature matching done, input=%d, candidates=%d, matched=%d, duration=%s", - installedPackageNames.size, - fDroidAppsAndSignatures.size, - fDroidUpdatablePackageNames.size, - formatDuration(nowMs() - signatureStartMs) - ) - return@coroutineScope fDroidUpdatablePackageNames } @@ -519,20 +404,11 @@ class UpdatesManagerImpl @Inject constructor( packageName: String, signature: String, ): Pair { - val baseApkStartMs = nowMs() val baseApkPath = appLoungePackageManager.getBaseApkPath(packageName) - val baseApkDuration = formatDuration(nowMs() - baseApkStartMs) - Timber.tag("FAHIM").i( - "Updates base APK path fetched, package=%s, duration=%s, hasPath=%s", - packageName, - baseApkDuration, - baseApkPath.isNotEmpty() - ) if (baseApkPath.isEmpty()) { return Pair(packageName, false) } - val verifyStartMs = nowMs() val verified = withContext(Dispatchers.IO) { ApkSignatureManager.verifyFdroidSignature( context, @@ -541,12 +417,6 @@ class UpdatesManagerImpl @Inject constructor( packageName, ) } - Timber.tag("FAHIM").i( - "Updates signature verified, package=%s, duration=%s, matched=%s", - packageName, - formatDuration(nowMs() - verifyStartMs), - verified - ) return Pair(packageName, verified) } @@ -589,22 +459,9 @@ class UpdatesManagerImpl @Inject constructor( val packageName = latestCleanapkApp.package_name val latestSignatureVersion = latestCleanapkApp.latest_downloaded_version - Timber.tag("FAHIM").i( - "Updates signature latest version, package=%s, version=%s", - packageName, - latestSignatureVersion - ) - val installedVersionCode = appLoungePackageManager.getVersionCode(packageName) val installedVersionName = appLoungePackageManager.getVersionName(packageName) - Timber.tag("FAHIM").i( - "Updates signature installed version, package=%s, code=%s, name=%s", - packageName, - installedVersionCode, - installedVersionName - ) - val latestSignatureVersionNumber = try { latestSignatureVersion.split("_")[1].toInt() } catch (e: Exception) { @@ -613,35 +470,18 @@ class UpdatesManagerImpl @Inject constructor( // Received list has build info of the latest version at the bottom. // We want it at the top. - val buildInfoStartMs = nowMs() val builds = handleNetworkResult { fDroidRepository.getBuildVersionInfo(packageName).asReversed() }.data - Timber.tag("FAHIM").i( - "Updates build info fetched, package=%s, duration=%s, buildCount=%d", - packageName, - formatDuration(nowMs() - buildInfoStartMs), - builds?.size ?: 0 - ) val matchingIndex = builds?.find { it.versionCode == installedVersionCode && it.versionName == installedVersionName }?.run { builds.indexOf(this) } ?: run { - Timber.tag("FAHIM").i( - "Updates build info match missing, package=%s", - packageName - ) return "" } - Timber.tag("FAHIM").i( - "Updates build info match found, package=%s, index=%d", - packageName, - 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: @@ -653,70 +493,4 @@ class UpdatesManagerImpl @Inject constructor( fun getApplicationCategoryPreference(): List { return applicationRepository.getSelectedAppTypes() } - - private inline fun logInstalledApps( - label: String, - fetch: () -> List, - ): List { - val startMs = nowMs() - val apps = fetch() - Timber.tag("FAHIM").i( - "Updates %s installed apps, count=%d, duration=%s", - label, - apps.size, - formatDuration(nowMs() - startMs) - ) - return apps - } - - private suspend fun logUpdatesFetch( - label: String, - appCount: Int, - fetch: suspend () -> Pair, ResultStatus>, - ): Pair, ResultStatus> { - val startMs = nowMs() - val result = fetch() - Timber.tag("FAHIM").i( - "Updates %s fetch done, apps=%d, duration=%s, status=%s", - label, - appCount, - formatDuration(nowMs() - startMs), - result.second - ) - return result - } - - private suspend fun logSystemAppsFetch( - label: String, - fetch: suspend () -> List, - ): List { - val startMs = nowMs() - val apps = fetch() - Timber.tag("FAHIM").i( - "Updates %s fetch done, apps=%d, duration=%s", - label, - apps.size, - formatDuration(nowMs() - startMs) - ) - return apps - } - - private fun nowMs(): Long { - return System.nanoTime() / NANOS_IN_MILLI - } - - private fun formatDuration(durationMs: Long): String { - val totalSeconds = durationMs / MILLIS_IN_SECOND - val milliseconds = durationMs % MILLIS_IN_SECOND - val seconds = totalSeconds % SECONDS_IN_MINUTE - val totalMinutes = totalSeconds / SECONDS_IN_MINUTE - val minutes = totalMinutes % MINUTES_IN_HOUR - val hours = totalMinutes / MINUTES_IN_HOUR - - return if (hours > 0) { - String.format(Locale.US, "%d:%02d:%02d.%03d", hours, minutes, seconds, milliseconds) - } else { - String.format(Locale.US, "%02d:%02d.%03d", minutes, seconds, milliseconds) - } - } } 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 8b3cbd4cf..36343f717 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 @@ -19,10 +19,8 @@ package foundation.e.apps.ui.updates import android.os.Bundle -import android.os.SystemClock import android.view.View import androidx.appcompat.app.AlertDialog -import androidx.core.view.isVisible import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle @@ -86,26 +84,14 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), ApplicationI private val appProgressViewModel: AppProgressViewModel by viewModels() private var isDownloadObserverAdded = false - private var updatesUiStartMs: Long = 0L - private var updatesLoadStartMs: Long = 0L - companion object { private const val SCROLL_TO_TOP_DELAY_MILLIS = 100L - private const val MILLIS_IN_SECOND = 1000L - private const val SECONDS_IN_MINUTE = 60L - private const val MINUTES_IN_HOUR = 60L } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) _binding = FragmentUpdatesBinding.bind(view) - updatesUiStartMs = SystemClock.elapsedRealtime() - Timber.tag("FAHIM").i( - "Updates UI created, sinceUi=%s", - formatDuration(0L) - ) - binding.button.isEnabled = false setupListening() @@ -157,16 +143,6 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), ApplicationI // Put system apps on top val appsToDisplay = appsUpdateList.sortedByDescending { it.isSystemApp } - val now = SystemClock.elapsedRealtime() - val sinceUi = now - updatesUiStartMs - val sinceLoad = if (updatesLoadStartMs == 0L) -1L else now - updatesLoadStartMs - Timber.tag("FAHIM").i( - "Updates list received, items=%d, sinceUi=%s, sinceLoad=%s", - appsToDisplay.size, - formatDuration(sinceUi), - formatDuration(sinceLoad) - ) - listAdapter?.setData(appsToDisplay) if (!isDownloadObserverAdded) { updateButtonAvailability() @@ -176,18 +152,6 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), ApplicationI stopLoadingUI() - val afterStop = SystemClock.elapsedRealtime() - val afterStopSinceUi = afterStop - updatesUiStartMs - val afterStopSinceLoad = - if (updatesLoadStartMs == 0L) -1L else afterStop - updatesLoadStartMs - Timber.tag("FAHIM").i( - "Updates UI ready, listVisible=%s, emptyVisible=%s, sinceUi=%s, sinceLoad=%s", - binding.recyclerView.isVisible, - binding.noUpdates.isVisible, - formatDuration(afterStopSinceUi), - formatDuration(afterStopSinceLoad) - ) - binding.recyclerView.postDelayed( { _binding?.recyclerView?.scrollToPosition(0) }, SCROLL_TO_TOP_DELAY_MILLIS @@ -353,11 +317,6 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), ApplicationI override fun loadData(authObjectList: List) { if (updatesViewModel.haveSourcesChanged()) { showLoadingUI() - updatesLoadStartMs = SystemClock.elapsedRealtime() - Timber.tag("FAHIM").i( - "Updates load started, sinceUi=%s", - formatDuration(updatesLoadStartMs - updatesUiStartMs) - ) updatesViewModel.loadUpdates() initUpdateAllButton() updateButtonAvailability() @@ -376,34 +335,6 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), ApplicationI binding.noUpdates.visibility = View.GONE binding.progressBar.visibility = View.VISIBLE binding.recyclerView.visibility = View.INVISIBLE - - val now = SystemClock.elapsedRealtime() - val sinceUi = now - updatesUiStartMs - val sinceLoad = if (updatesLoadStartMs == 0L) -1L else now - updatesLoadStartMs - Timber.tag("FAHIM").i( - "Updates loading UI shown, sinceUi=%s, sinceLoad=%s", - formatDuration(sinceUi), - formatDuration(sinceLoad) - ) - } - - private fun formatDuration(durationMs: Long): String { - if (durationMs < 0L) { - return "n/a" - } - - val totalSeconds = durationMs / MILLIS_IN_SECOND - val milliseconds = durationMs % MILLIS_IN_SECOND - val seconds = totalSeconds % SECONDS_IN_MINUTE - val totalMinutes = totalSeconds / SECONDS_IN_MINUTE - val minutes = totalMinutes % MINUTES_IN_HOUR - val hours = totalMinutes / MINUTES_IN_HOUR - - return if (hours > 0) { - String.format(Locale.US, "%d:%02d:%02d.%03d", hours, minutes, seconds, milliseconds) - } else { - String.format(Locale.US, "%02d:%02d.%03d", minutes, seconds, milliseconds) - } } override fun stopLoadingUI() { -- GitLab