From 5ca87a9b992b7fce750d6dce55949cc57a9ce0ae Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Tue, 16 Dec 2025 15:35:27 +0600 Subject: [PATCH 1/2] fix: fix showing progress values (download vs. total size and percentage) on app details screen Previously, progress was posted only when downloadingIds.size == cursor.count in DownloadProgressLD. When the cursor was partial or null), no progress was emitted, causing the UI to show no progress data, despite available data. With this fix, removed dependency on cursor row count; always post progress after processing returned rows. --- .../download/data/DownloadProgressLD.kt | 93 +++++++++++-------- 1 file changed, 54 insertions(+), 39 deletions(-) diff --git a/app/src/main/java/foundation/e/apps/install/download/data/DownloadProgressLD.kt b/app/src/main/java/foundation/e/apps/install/download/data/DownloadProgressLD.kt index d017c2d65..754f3ec1c 100644 --- a/app/src/main/java/foundation/e/apps/install/download/data/DownloadProgressLD.kt +++ b/app/src/main/java/foundation/e/apps/install/download/data/DownloadProgressLD.kt @@ -75,46 +75,61 @@ class DownloadProgressLD @Inject constructor( } private fun findDownloadProgress(downloadingIds: MutableList) { - downloadManager.query(downloadManagerQuery.setFilterById(*downloadingIds.toLongArray())) - .use { cursor -> - cursor.moveToFirst() - while (!cursor.isAfterLast) { - val id = - cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_ID)) - val status = - cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_STATUS)) - val totalSizeBytes = - cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_TOTAL_SIZE_BYTES)) - val bytesDownloadedSoFar = - cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_BYTES_DOWNLOADED_SO_FAR)) - - downloadProgress.downloadId = id - - if (!downloadProgress.totalSizeBytes.containsKey(id) || - downloadProgress.totalSizeBytes[id] != totalSizeBytes - ) { - downloadProgress.totalSizeBytes[id] = totalSizeBytes - } - - if (!downloadProgress.bytesDownloadedSoFar.containsKey(id) || - downloadProgress.bytesDownloadedSoFar[id] != bytesDownloadedSoFar - ) { - downloadProgress.bytesDownloadedSoFar[id] = bytesDownloadedSoFar - } - - downloadProgress.status[id] = - status == DownloadManager.STATUS_SUCCESSFUL || status == DownloadManager.STATUS_FAILED - - if (downloadingIds.size == cursor.count) { - postValue(downloadProgress) - } - - if (downloadingIds.isEmpty()) { - cancel() - } - cursor.moveToNext() - } + val idsToQuery = downloadingIds.toLongArray() + if (idsToQuery.isEmpty()) { + Timber.i("No pending download ids to query; skipping progress refresh.") + return + } + + val cursor = downloadManager.query(downloadManagerQuery.setFilterById(*idsToQuery)) + if (cursor == null) { + Timber.w("DownloadManager returned null cursor for ids $downloadingIds; posting cached progress to avoid stalling UI.") + postValue(downloadProgress) + return + } + + cursor.use { safeCursor -> + val foundIds = mutableSetOf() + + if (!safeCursor.moveToFirst()) { + Timber.w("Download cursor empty for ids $downloadingIds; posting cached progress to stay responsive.") + postValue(downloadProgress) + return } + + do { + val id = safeCursor.getLong(safeCursor.getColumnIndexOrThrow(COLUMN_ID)) + val status = safeCursor.getInt(safeCursor.getColumnIndexOrThrow(COLUMN_STATUS)) + val totalSizeBytes = + safeCursor.getLong(safeCursor.getColumnIndexOrThrow(COLUMN_TOTAL_SIZE_BYTES)) + val bytesDownloadedSoFar = + safeCursor.getLong(safeCursor.getColumnIndexOrThrow(COLUMN_BYTES_DOWNLOADED_SO_FAR)) + + foundIds.add(id) + downloadProgress.downloadId = id + + if (!downloadProgress.totalSizeBytes.containsKey(id) || + downloadProgress.totalSizeBytes[id] != totalSizeBytes + ) { + downloadProgress.totalSizeBytes[id] = totalSizeBytes + } + + if (!downloadProgress.bytesDownloadedSoFar.containsKey(id) || + downloadProgress.bytesDownloadedSoFar[id] != bytesDownloadedSoFar + ) { + downloadProgress.bytesDownloadedSoFar[id] = bytesDownloadedSoFar + } + + downloadProgress.status[id] = + status == DownloadManager.STATUS_SUCCESSFUL || status == DownloadManager.STATUS_FAILED + + if (downloadingIds.isEmpty()) { + cancel() + } + } while (safeCursor.moveToNext()) + + postValue(downloadProgress) + } } override fun onInactive() { -- GitLab From 200674ebfbb5b503811acb6a68389f64088db357 Mon Sep 17 00:00:00 2001 From: Fahim Masud Choudhury Date: Tue, 16 Dec 2025 16:50:35 +0600 Subject: [PATCH 2/2] fix: fix progress rendering on Home, Search, and Updates screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When multiple downloads are in flight, show progress in the list by validating progress against all download IDs for an app instead of a single downloadProgress.downloadId. Previously, validation compared the last cursor row’s downloadId to the app’s ID. With parallel or multi-part downloads, most apps failed validation, so percentage text never updated; UI stayed on “Cancel/Installing” until completion. Replaced single-ID equality check with set intersection: pull the app’s downloadIdMap keys and accept progress when any of those IDs appears in the aggregated progress maps. --- app/detekt-baseline.xml | 1 - .../foundation/e/apps/data/enums/Status.kt | 11 ++- .../e/apps/data/install/AppManagerWrapper.kt | 18 +++- .../download/data/DownloadProgressLD.kt | 84 ++++++++++--------- .../ApplicationListFragment.kt | 28 ++++--- .../foundation/e/apps/ui/home/HomeFragment.kt | 7 +- .../e/apps/ui/search/SearchFragment.kt | 28 ++++--- .../e/apps/ui/updates/UpdatesFragment.kt | 28 ++++--- 8 files changed, 118 insertions(+), 87 deletions(-) diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml index 1079e691f..8494db6dd 100644 --- a/app/detekt-baseline.xml +++ b/app/detekt-baseline.xml @@ -147,7 +147,6 @@ ReturnCount:SystemAppsUpdatesRepository.kt$SystemAppsUpdatesRepository$private suspend fun getApplication( packageName: String, releaseType: OsReleaseType, sdkLevel: Int, device: String, ): Application? ReturnCount:SystemAppsUpdatesRepository.kt$SystemAppsUpdatesRepository$private suspend fun getReleaseDetailsUrl( systemAppProject: SystemAppProject, releaseType: OsReleaseType, ): String? ReturnCount:UpdatesManagerImpl.kt$UpdatesManagerImpl$private suspend fun calculateSignatureVersion(latestCleanapkApp: Application): String - SpreadOperator:DownloadProgressLD.kt$DownloadProgressLD$(*downloadingIds.toLongArray()) SpreadOperator:EglExtensionProvider.kt$EglExtensionProvider$(*`as`) SpreadOperator:NativeDeviceInfoProviderModule.kt$NativeDeviceInfoProviderModule$(*context.assets.locales) SpreadOperator:NativeDeviceInfoProviderModule.kt$NativeDeviceInfoProviderModule$(*systemSharedLibraryNames) diff --git a/app/src/main/java/foundation/e/apps/data/enums/Status.kt b/app/src/main/java/foundation/e/apps/data/enums/Status.kt index 849f305f2..f16de5cc8 100644 --- a/app/src/main/java/foundation/e/apps/data/enums/Status.kt +++ b/app/src/main/java/foundation/e/apps/data/enums/Status.kt @@ -29,5 +29,14 @@ enum class Status { BLOCKED, INSTALLATION_ISSUE, AWAITING, - PURCHASE_NEEDED + PURCHASE_NEEDED; + + companion object { + val downloadStatuses = setOf( + QUEUED, + AWAITING, + DOWNLOADING, + DOWNLOADED + ) + } } diff --git a/app/src/main/java/foundation/e/apps/data/install/AppManagerWrapper.kt b/app/src/main/java/foundation/e/apps/data/install/AppManagerWrapper.kt index 81d54847a..fd82ddfd4 100644 --- a/app/src/main/java/foundation/e/apps/data/install/AppManagerWrapper.kt +++ b/app/src/main/java/foundation/e/apps/data/install/AppManagerWrapper.kt @@ -143,8 +143,22 @@ class AppManagerWrapper @Inject constructor( application: Application, downloadProgress: DownloadProgress ): Boolean { - val download = getFusedDownload(downloadProgress.downloadId) - return download.id == application._id + val download = getDownloadList().singleOrNull { + it.id == application._id && it.packageName == application.package_name + } ?: return false + + /* + * We cannot rely on a single downloadId because DownloadProgress aggregates + * multiple ids and downloadId is overwritten while iterating. + * Validation instead checks whether any of the app's download ids are present + * in the progress maps, which makes progress computation resilient to + * concurrent multi-part downloads. + */ + val appDownloadIds = download.downloadIdMap.keys + return appDownloadIds.any { id -> + downloadProgress.totalSizeBytes.containsKey(id) || + downloadProgress.bytesDownloadedSoFar.containsKey(id) + } } fun handleRatingFormat(rating: Double): String? { diff --git a/app/src/main/java/foundation/e/apps/install/download/data/DownloadProgressLD.kt b/app/src/main/java/foundation/e/apps/install/download/data/DownloadProgressLD.kt index 754f3ec1c..6e7ffe34f 100644 --- a/app/src/main/java/foundation/e/apps/install/download/data/DownloadProgressLD.kt +++ b/app/src/main/java/foundation/e/apps/install/download/data/DownloadProgressLD.kt @@ -23,6 +23,7 @@ import android.app.DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR import android.app.DownloadManager.COLUMN_ID import android.app.DownloadManager.COLUMN_STATUS import android.app.DownloadManager.COLUMN_TOTAL_SIZE_BYTES +import android.database.Cursor import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LiveData @@ -67,69 +68,70 @@ class DownloadProgressLD @Inject constructor( try { findDownloadProgress(downloadingIds) } catch (e: Exception) { - Timber.e("downloading Ids: $downloadingIds ${e.localizedMessage}") + Timber.w("downloading Ids: $downloadingIds $e") } delay(20) } } } + @Suppress("SpreadOperator") // DownloadManager#Query requires vararg ids; unavoidable spread. private fun findDownloadProgress(downloadingIds: MutableList) { val idsToQuery = downloadingIds.toLongArray() if (idsToQuery.isEmpty()) { - Timber.i("No pending download ids to query; skipping progress refresh.") return } - val cursor = downloadManager.query(downloadManagerQuery.setFilterById(*idsToQuery)) - if (cursor == null) { - Timber.w("DownloadManager returned null cursor for ids $downloadingIds; posting cached progress to avoid stalling UI.") + downloadManager.query(downloadManagerQuery.setFilterById(*idsToQuery)) + ?.use { safeCursor -> + processCursor(safeCursor, downloadingIds) + } + ?: run { + Timber.w("DownloadManager returned null cursor for ids $downloadingIds") + postValue(downloadProgress) + } + } + + private fun processCursor( + cursor: Cursor, + downloadingIds: List + ) { + if (!cursor.moveToFirst()) { postValue(downloadProgress) return } - cursor.use { safeCursor -> - val foundIds = mutableSetOf() + do { + val id = cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_ID)) + val status = cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_STATUS)) + val totalSizeBytes = + cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_TOTAL_SIZE_BYTES)) + val bytesDownloadedSoFar = + cursor.getLong(cursor.getColumnIndexOrThrow(COLUMN_BYTES_DOWNLOADED_SO_FAR)) - if (!safeCursor.moveToFirst()) { - Timber.w("Download cursor empty for ids $downloadingIds; posting cached progress to stay responsive.") - postValue(downloadProgress) - return - } + downloadProgress.downloadId = id - do { - val id = safeCursor.getLong(safeCursor.getColumnIndexOrThrow(COLUMN_ID)) - val status = safeCursor.getInt(safeCursor.getColumnIndexOrThrow(COLUMN_STATUS)) - val totalSizeBytes = - safeCursor.getLong(safeCursor.getColumnIndexOrThrow(COLUMN_TOTAL_SIZE_BYTES)) - val bytesDownloadedSoFar = - safeCursor.getLong(safeCursor.getColumnIndexOrThrow(COLUMN_BYTES_DOWNLOADED_SO_FAR)) - - foundIds.add(id) - downloadProgress.downloadId = id - - if (!downloadProgress.totalSizeBytes.containsKey(id) || - downloadProgress.totalSizeBytes[id] != totalSizeBytes - ) { - downloadProgress.totalSizeBytes[id] = totalSizeBytes - } + if (!downloadProgress.totalSizeBytes.containsKey(id) || + downloadProgress.totalSizeBytes[id] != totalSizeBytes + ) { + downloadProgress.totalSizeBytes[id] = totalSizeBytes + } - if (!downloadProgress.bytesDownloadedSoFar.containsKey(id) || - downloadProgress.bytesDownloadedSoFar[id] != bytesDownloadedSoFar - ) { - downloadProgress.bytesDownloadedSoFar[id] = bytesDownloadedSoFar - } + if (!downloadProgress.bytesDownloadedSoFar.containsKey(id) || + downloadProgress.bytesDownloadedSoFar[id] != bytesDownloadedSoFar + ) { + downloadProgress.bytesDownloadedSoFar[id] = bytesDownloadedSoFar + } - downloadProgress.status[id] = - status == DownloadManager.STATUS_SUCCESSFUL || status == DownloadManager.STATUS_FAILED + downloadProgress.status[id] = + status == DownloadManager.STATUS_SUCCESSFUL || status == DownloadManager.STATUS_FAILED - if (downloadingIds.isEmpty()) { - cancel() - } - } while (safeCursor.moveToNext()) + if (downloadingIds.isEmpty()) { + cancel() + } + } while (cursor.moveToNext()) - postValue(downloadProgress) - } + postValue(downloadProgress) } override fun onInactive() { diff --git a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListFragment.kt b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListFragment.kt index a270be02a..19ae77fc1 100644 --- a/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/applicationlist/ApplicationListFragment.kt @@ -303,19 +303,21 @@ class ApplicationListFragment : val adapter = recyclerView.adapter as ApplicationListRVAdapter viewLifecycleOwner.lifecycleScope.launch { adapter.currentList.forEach { fusedApp -> - if (fusedApp.status == Status.DOWNLOADING) { - val progress = - appProgressViewModel.calculateProgress(fusedApp, downloadProgress) - if (progress == -1) { - return@forEach - } - val viewHolder = recyclerView.findViewHolderForAdapterPosition( - adapter.currentList.indexOf(fusedApp) - ) - viewHolder?.let { - (viewHolder as ApplicationListRVAdapter.ViewHolder).binding.installButton.text = - String.format(Locale.getDefault(), "%d%%", progress) - } + if (fusedApp.status !in Status.downloadStatuses) { + return@forEach + } + + val progress = + appProgressViewModel.calculateProgress(fusedApp, downloadProgress) + if (progress == -1) { + return@forEach + } + val viewHolder = recyclerView.findViewHolderForAdapterPosition( + adapter.currentList.indexOf(fusedApp) + ) + viewHolder?.let { + (viewHolder as ApplicationListRVAdapter.ViewHolder).binding.installButton.text = + String.format(Locale.getDefault(), "%d%%", progress) } } } diff --git a/app/src/main/java/foundation/e/apps/ui/home/HomeFragment.kt b/app/src/main/java/foundation/e/apps/ui/home/HomeFragment.kt index e6c0bda41..f8e50f87e 100644 --- a/app/src/main/java/foundation/e/apps/ui/home/HomeFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/home/HomeFragment.kt @@ -170,9 +170,10 @@ class HomeFragment : Fragment(R.layout.fragment_home), ApplicationInstaller { childRV: RecyclerView ) { adapter.currentList.forEach { fusedApp -> - if (fusedApp.status != Status.DOWNLOADING) { + if (fusedApp.status !in Status.downloadStatuses) { return@forEach } + val progress = appProgressViewModel.calculateProgress(fusedApp, downloadProgress) if (progress == -1) { @@ -182,8 +183,8 @@ class HomeFragment : Fragment(R.layout.fragment_home), ApplicationInstaller { adapter.currentList.indexOf(fusedApp) ) childViewHolder?.let { - (childViewHolder as HomeChildRVAdapter.ViewHolder).binding.installButton.text = - String.format(Locale.getDefault(), "%d%%", progress) + (childViewHolder as HomeChildRVAdapter.ViewHolder).binding.installButton.text = + String.format(Locale.getDefault(), "%d%%", progress) } } } diff --git a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt index ef32ecc9e..8e1afdf90 100644 --- a/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt +++ b/app/src/main/java/foundation/e/apps/ui/search/SearchFragment.kt @@ -346,19 +346,21 @@ class SearchFragment : Fragment(R.layout.fragment_search), ApplicationInstaller, val adapter = recyclerView?.adapter as ApplicationListRVAdapter viewLifecycleOwner.lifecycleScope.launch { adapter.currentList.forEach { fusedApp -> - if (fusedApp.status == Status.DOWNLOADING) { - val progress = - appProgressViewModel.calculateProgress(fusedApp, downloadProgress) - if (progress == -1) { - return@forEach - } - val viewHolder = recyclerView?.findViewHolderForAdapterPosition( - adapter.currentList.indexOf(fusedApp) - ) - viewHolder?.let { - (viewHolder as ApplicationListRVAdapter.ViewHolder).binding.installButton.text = - String.format(Locale.getDefault(), "%d%%", progress) - } + if (fusedApp.status !in Status.downloadStatuses) { + return@forEach + } + + val progress = + appProgressViewModel.calculateProgress(fusedApp, downloadProgress) + if (progress == -1) { + return@forEach + } + val viewHolder = recyclerView?.findViewHolderForAdapterPosition( + adapter.currentList.indexOf(fusedApp) + ) + viewHolder?.let { + (viewHolder as ApplicationListRVAdapter.ViewHolder).binding.installButton.text = + String.format(Locale.getDefault(), "%d%%", progress) } } } 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 f85576e15..1f900c3f7 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 @@ -354,19 +354,21 @@ class UpdatesFragment : TimeoutFragment(R.layout.fragment_updates), ApplicationI val adapter = recyclerView.adapter as ApplicationListRVAdapter viewLifecycleOwner.lifecycleScope.launch { adapter.currentList.forEach { fusedApp -> - if (fusedApp.status == Status.DOWNLOADING) { - val progress = - appProgressViewModel.calculateProgress(fusedApp, downloadProgress) - if (progress == -1) { - return@forEach - } - val viewHolder = recyclerView.findViewHolderForAdapterPosition( - adapter.currentList.indexOf(fusedApp) - ) - viewHolder?.let { - (viewHolder as ApplicationListRVAdapter.ViewHolder).binding.installButton.text = - String.format(Locale.getDefault(), "%d%%", progress) - } + if (fusedApp.status !in Status.downloadStatuses) { + return@forEach + } + + val progress = + appProgressViewModel.calculateProgress(fusedApp, downloadProgress) + if (progress == -1) { + return@forEach + } + val viewHolder = recyclerView.findViewHolderForAdapterPosition( + adapter.currentList.indexOf(fusedApp) + ) + viewHolder?.let { + (viewHolder as ApplicationListRVAdapter.ViewHolder).binding.installButton.text = + String.format(Locale.getDefault(), "%d%%", progress) } } } -- GitLab