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) } } }