Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<OCFile>?) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2026 Alper Ozturk <alper.ozturk@nextcloud.com>
* 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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -40,9 +41,8 @@ class FolderDownloadWorker(
const val FOLDER_ID = "FOLDER_ID"
const val ACCOUNT_NAME = "ACCOUNT_NAME"

private val pendingDownloads: MutableSet<Long> = ConcurrentHashMap.newKeySet<Long>()

fun isDownloading(id: Long): Boolean = pendingDownloads.contains(id)
private val _activeFolders = MutableStateFlow<Set<FolderDownloadState>>(emptySet())
val activeFolders: StateFlow<Set<FolderDownloadState>> = _activeFolders
}

private val notificationManager = FolderDownloadWorkerNotificationManager(context, viewThemeUtils)
Expand Down Expand Up @@ -79,7 +79,7 @@ class FolderDownloadWorker(

trySetForeground(folder)

pendingDownloads.add(folder.fileId)
_activeFolders.value += FolderDownloadState.Downloading(folderID)

val downloadHelper = FileDownloadHelper.instance()

Expand Down Expand Up @@ -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()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ fun FileDataStorageManager.getDecryptedPath(file: OCFile): String {
.joinToString(OCFile.PATH_SEPARATOR)
}

suspend fun FileDataStorageManager.getSubfiles(id: Long, accountName: String): List<OCFile> =
fun FileDataStorageManager.getSubfiles(id: Long, accountName: String): List<OCFile> =
fileDao.getSubfiles(id, accountName).map {
createFileInstance(it)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -279,6 +283,7 @@ class FileDisplayActivity :
startMetadataSyncForRoot()
handleBackPress()
setupDrawer(menuItemId)
observeFolderDownloadWorker()
}

/**
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -2426,10 +2436,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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -869,6 +871,7 @@ public void updateAdapter(List<OCFile> newFiles, OCFile directory) {

mFiles = new ArrayList<>(newFiles);
mFilesAll.clear();
ocFileListDelegate.clearFolderDownloadStates();
mFilesAll.addAll(mFiles);

if (directory != null) {
Expand Down Expand Up @@ -1061,4 +1064,33 @@ public void removeAllFiles() {
mFilesAll.clear();
notifyDataSetChanged();
}

public void notifyFolderDownloadStates(Set<FolderDownloadState> states) {
Set<FolderDownloadState> previousStates = new HashSet<>(ocFileListDelegate.getFolderDownloadStates());

ocFileListDelegate.addFolderDownloadStates(states);

Set<Long> 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -68,6 +68,7 @@ class OCFileListDelegate(
private val asyncTasks: MutableList<ThumbnailsCacheManager.ThumbnailGenerationTask> = ArrayList()
private val ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
private val galleryImageGenerationJob = GalleryImageGenerationJob(user, storageManager)
private val folderDownloadStates = mutableSetOf<FolderDownloadState>()

fun setHighlightedItem(highlightedItem: OCFile?) {
this.highlightedItem = highlightedItem
Expand Down Expand Up @@ -315,45 +316,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
}
}
}
Expand Down Expand Up @@ -409,6 +411,18 @@ class OCFileListDelegate(
showShareAvatar = bool
}

fun addFolderDownloadStates(ids: Set<FolderDownloadState>) {
folderDownloadStates.clear()
folderDownloadStates.addAll(ids)

Log_OC.d(TAG, "downloading folders - added current: $folderDownloadStates")
}

fun getFolderDownloadStates(): List<FolderDownloadState> = 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()

Expand Down
Loading