diff --git a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt index 86d02064c7a3..13dd20a4efe6 100644 --- a/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt +++ b/app/src/main/java/com/nextcloud/client/database/dao/FileDao.kt @@ -99,7 +99,7 @@ interface FileDao { ORDER BY ${ProviderTableMeta.FILE_DEFAULT_SORT_ORDER} """ ) - suspend fun getSubfiles( + fun getSubfiles( parentId: Long, accountName: String, dirType: String = MimeType.DIRECTORY, diff --git a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadHelper.kt index 76f4b99ae7b3..6fbe1e7ac151 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/download/FileDownloadHelper.kt @@ -9,7 +9,6 @@ package com.nextcloud.client.jobs.download import com.nextcloud.client.account.User import com.nextcloud.client.jobs.BackgroundJobManager -import com.nextcloud.client.jobs.folderDownload.FolderDownloadWorker import com.owncloud.android.MainApp import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile @@ -47,11 +46,7 @@ class FileDownloadHelper { return false } - return if (file.isFolder) { - FolderDownloadWorker.isDownloading(file.fileId) - } else { - FileDownloadWorker.isDownloading(user.accountName, file.fileId) - } + return FileDownloadWorker.isDownloading(user.accountName, file.fileId) } fun cancelPendingOrCurrentDownloads(user: User?, files: List?) { diff --git a/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadState.kt b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadState.kt new file mode 100644 index 000000000000..c4553f5a24d8 --- /dev/null +++ b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadState.kt @@ -0,0 +1,13 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.client.jobs.folderDownload + +sealed class FolderDownloadState(open val id: Long) { + data class Downloading(override val id: Long) : FolderDownloadState(id) + data class Removed(override val id: Long) : FolderDownloadState(id) +} diff --git a/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt index 337ff1bf6ee7..e5d9e95030a4 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadWorker.kt @@ -24,8 +24,9 @@ import com.owncloud.android.operations.DownloadType import com.owncloud.android.ui.helpers.FileOperationsHelper import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.withContext -import java.util.concurrent.ConcurrentHashMap @Suppress("LongMethod", "TooGenericExceptionCaught") class FolderDownloadWorker( @@ -40,9 +41,8 @@ class FolderDownloadWorker( const val FOLDER_ID = "FOLDER_ID" const val ACCOUNT_NAME = "ACCOUNT_NAME" - private val pendingDownloads: MutableSet = ConcurrentHashMap.newKeySet() - - fun isDownloading(id: Long): Boolean = pendingDownloads.contains(id) + private val _activeFolders = MutableStateFlow>(emptySet()) + val activeFolders: StateFlow> = _activeFolders } private val notificationManager = FolderDownloadWorkerNotificationManager(context, viewThemeUtils) @@ -79,7 +79,7 @@ class FolderDownloadWorker( trySetForeground(folder) - pendingDownloads.add(folder.fileId) + _activeFolders.value += FolderDownloadState.Downloading(folderID) val downloadHelper = FileDownloadHelper.instance() @@ -137,7 +137,7 @@ class FolderDownloadWorker( Result.failure() } finally { WorkerStateObserver.send(WorkerState.FolderDownloadCompleted(folder)) - pendingDownloads.remove(folder.fileId) + _activeFolders.value -= FolderDownloadState.Downloading(folderID) notificationManager.dismiss() } } diff --git a/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt index 77f662d76c11..6567d7c5b559 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/FileDataStorageManagerExtensions.kt @@ -44,7 +44,7 @@ fun FileDataStorageManager.getDecryptedPath(file: OCFile): String { .joinToString(OCFile.PATH_SEPARATOR) } -suspend fun FileDataStorageManager.getSubfiles(id: Long, accountName: String): List = +fun FileDataStorageManager.getSubfiles(id: Long, accountName: String): List = fileDao.getSubfiles(id, accountName).map { createFileInstance(it) } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index 651f4741639d..3f4d1817af3c 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -47,7 +47,9 @@ import androidx.appcompat.widget.SearchView import androidx.core.view.MenuItemCompat import androidx.core.view.isVisible import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.google.android.material.appbar.AppBarLayout import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -65,6 +67,8 @@ import com.nextcloud.client.jobs.download.FileDownloadHelper import com.nextcloud.client.jobs.download.FileDownloadWorker import com.nextcloud.client.jobs.download.FileDownloadWorker.Companion.getDownloadAddedMessage import com.nextcloud.client.jobs.download.FileDownloadWorker.Companion.getDownloadFinishMessage +import com.nextcloud.client.jobs.folderDownload.FolderDownloadState +import com.nextcloud.client.jobs.folderDownload.FolderDownloadWorker import com.nextcloud.client.jobs.upload.FileUploadBroadcastManager import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.client.jobs.upload.FileUploadWorker @@ -279,6 +283,7 @@ class FileDisplayActivity : startMetadataSyncForRoot() handleBackPress() setupDrawer(menuItemId) + observeFolderDownloadWorker() } /** @@ -2130,6 +2135,11 @@ class FileDisplayActivity : } supportInvalidateOptionsMenu() fetchRecommendedFilesIfNeeded(ignoreETag = true, currentDir) + + if (removedFile.isFolder) { + val deletedFolderDownloadState = FolderDownloadState.Removed(removedFile.fileId) + listOfFilesFragment?.adapter?.notifyFolderDownloadStates(setOf(deletedFolderDownloadState)) + } } else { if (result.isSslRecoverableException) { mLastSslUntrustedServerResult = result @@ -2411,10 +2421,21 @@ class FileDisplayActivity : } } + private fun observeFolderDownloadWorker() { + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + FolderDownloadWorker.activeFolders.collect { workerStates -> + Log_OC.d(TAG, "currently downloading: ${workerStates.size}") + listOfFilesFragment?.adapter?.notifyFolderDownloadStates(workerStates) + } + } + } + } + private fun requestForDownload(file: OCFile, downloadBehaviour: String, packageName: String, activityName: String) { val currentUser = user.orElseThrow(Supplier { RuntimeException() }) - if (!FileDownloadHelper.Companion.instance().isDownloading(currentUser, file)) { - FileDownloadHelper.Companion.instance().downloadFile( + if (!FileDownloadHelper.instance().isDownloading(currentUser, file)) { + FileDownloadHelper.instance().downloadFile( currentUser, file, downloadBehaviour, diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java index c93624ba352c..02b6966f4513 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java @@ -30,6 +30,7 @@ import com.nextcloud.android.common.ui.theme.utils.ColorRole; import com.nextcloud.client.account.User; import com.nextcloud.client.database.entity.OfflineOperationEntity; +import com.nextcloud.client.jobs.folderDownload.FolderDownloadState; import com.nextcloud.client.jobs.upload.FileUploadHelper; import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.model.OfflineOperationType; @@ -75,6 +76,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Date; +import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Set; @@ -869,6 +871,7 @@ public void updateAdapter(List newFiles, OCFile directory) { mFiles = new ArrayList<>(newFiles); mFilesAll.clear(); + ocFileListDelegate.clearFolderDownloadStates(); mFilesAll.addAll(mFiles); if (directory != null) { @@ -1061,4 +1064,33 @@ public void removeAllFiles() { mFilesAll.clear(); notifyDataSetChanged(); } + + public void notifyFolderDownloadStates(Set states) { + Set previousStates = new HashSet<>(ocFileListDelegate.getFolderDownloadStates()); + + ocFileListDelegate.addFolderDownloadStates(states); + + Set changedFileIds = new HashSet<>(); + states.forEach(state -> changedFileIds.add(state.getId())); + previousStates.forEach(state -> changedFileIds.add(state.getId())); + + changedFileIds.forEach(fileId -> { + OCFile file = findOCFile(fileId); + if (file != null) { + int position = getItemPosition(file); + if (position != -1) { + notifyItemChanged(position); + } + } + }); + } + + private OCFile findOCFile(long id) { + for (OCFile file : mFiles) { + if (file.getFileId() == id) { + return file; + } + } + return null; + } } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt index 343659e8938a..07a1d6dc649c 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt @@ -15,6 +15,7 @@ import com.elyeproj.loaderviewlibrary.LoaderImageView import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.account.User import com.nextcloud.client.jobs.download.FileDownloadHelper +import com.nextcloud.client.jobs.folderDownload.FolderDownloadState import com.nextcloud.client.jobs.gallery.GalleryImageGenerationJob import com.nextcloud.client.jobs.gallery.GalleryImageGenerationListener import com.nextcloud.client.jobs.upload.FileUploadHelper @@ -43,7 +44,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext @Suppress("LongParameterList", "TooManyFunctions") class OCFileListDelegate( @@ -68,6 +68,7 @@ class OCFileListDelegate( private val asyncTasks: MutableList = ArrayList() private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val galleryImageGenerationJob = GalleryImageGenerationJob(user, storageManager) + private val folderDownloadStates = mutableSetOf() fun setHighlightedItem(highlightedItem: OCFile?) { this.highlightedItem = highlightedItem @@ -323,45 +324,46 @@ class OCFileListDelegate( } } - private suspend fun isFolderFullyDownloaded(file: OCFile): Boolean = withContext(Dispatchers.IO) { - file.isFolder && - storageManager.getSubfiles(file.fileId, user.accountName) - .takeIf { it.isNotEmpty() } - ?.all { it.isDown } == true - } + private fun isFolderFullyDownloaded(file: OCFile): Boolean = file.isFolder && + storageManager.getSubfiles(file.fileId, user.accountName) + .takeIf { it.isNotEmpty() } + ?.all { it.isDown } == true private fun isSynchronizing(file: OCFile): Boolean { val operationsServiceBinder = transferServiceGetter.operationsServiceBinder val fileDownloadHelper = FileDownloadHelper.instance() + val isDownloadingFolder = + folderDownloadStates.any { it is FolderDownloadState.Downloading && it.id == file.fileId } return operationsServiceBinder?.isSynchronizing(user, file) == true || fileDownloadHelper.isDownloading(user, file) || + isDownloadingFolder || fileUploadHelper.isUploading(file.remotePath, user.accountName) } private fun showLocalFileIndicator(file: OCFile, holder: ListViewHolder) { - ioScope.launch { - val isFullyDownloaded = isFolderFullyDownloaded(file) - val isSyncing = isSynchronizing(file) - val hasConflict = (file.etagInConflict != null) - val isDown = file.isDown - - val icon = when { - isSyncing -> R.drawable.ic_synchronizing - hasConflict -> R.drawable.ic_synchronizing_error - isDown || isFullyDownloaded -> R.drawable.ic_synced - else -> null - } + val isFullyDownloaded = isFolderFullyDownloaded(file) + val isSyncing = isSynchronizing(file) + val hasConflict = (file.etagInConflict != null) + val isDown = file.isDown + val isRemoved = folderDownloadStates.any { + it is FolderDownloadState.Removed && it.id == file.fileId + } - withContext(Dispatchers.Main) { - holder.localFileIndicator.run { - if (icon != null && showMetadata) { - setImageResource(icon) - visibility = View.VISIBLE - } else { - visibility = View.GONE - } - } + val icon = when { + isSyncing -> R.drawable.ic_synchronizing + hasConflict -> R.drawable.ic_synchronizing_error + isDown || isFullyDownloaded -> R.drawable.ic_synced + isRemoved -> null + else -> null + } + + holder.localFileIndicator.run { + if (icon != null && showMetadata) { + setImageResource(icon) + visibility = View.VISIBLE + } else { + visibility = View.GONE } } } @@ -418,6 +420,18 @@ class OCFileListDelegate( showShareAvatar = bool } + fun addFolderDownloadStates(ids: Set) { + folderDownloadStates.clear() + folderDownloadStates.addAll(ids) + + Log_OC.d(TAG, "downloading folders - added current: $folderDownloadStates") + } + + fun getFolderDownloadStates(): List = folderDownloadStates.toList() + + // only clear remove states since downloading states added and removed via worker + fun clearFolderDownloadStates() = folderDownloadStates.removeIf { it is FolderDownloadState.Removed } + fun cleanup() { ioScope.cancel()