diff --git a/app/detekt-baseline.xml b/app/detekt-baseline.xml
index 1079e691f83bcb7d8cb4ea32ed478780b165cb57..8494db6ddc8a912556ca1dc9acd7d5d49377cef2 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 849f305f25a878fd8b51b9202075612cb9b200c5..f16de5cc8cba7c61f5babc844b0d20bf74dd5e77 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 81d54847a999e76b8897d1e81b23b845572aea8c..fd82ddfd4655c85d0fc45236ac3af8b5289487ac 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 d017c2d65964cf601f0d8389b1d2424c9bbb898c..6e7ffe34fedbb9ece883dc2b8ea62731ce881a61 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,54 +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) {
- 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()) {
+ return
+ }
+
+ 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
+ }
+
+ 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))
+
+ 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 (cursor.moveToNext())
+
+ 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 a270be02a066b7bb8fa1416143da914727578037..19ae77fc1bfc6bf4e205b41a3768044e82c6759e 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 e6c0bda41aa00216a29229f0b20c2758cb4e615a..f8e50f87efeb46036ed4901eae9ba1bae4f470bc 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 ef32ecc9e74513a990ddd27bc946a633f5cf84a7..8e1afdf906d6c8de7ed0ff16dac889ec2294a812 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 f85576e1538e11daa53be3b6d8354cad6ac14eea..1f900c3f793088b4ed14815e185eac70aad862e8 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)
}
}
}