diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt index 1fe963ad7c34..166b9fc0334d 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -338,7 +338,6 @@ class BackgroundJobFactory @Inject constructor( accountManager, context, viewThemeUtils.get(), - localBroadcastManager.get(), params ) diff --git a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt index 86b49c3b47fb..3e110c414e42 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobManagerImpl.kt @@ -926,6 +926,8 @@ internal class BackgroundJobManagerImpl( val request = oneTimeRequestBuilder(FolderDownloadWorker::class, JOB_DOWNLOAD_FOLDER) .addTag(JOB_DOWNLOAD_FOLDER) + .addTag(FolderDownloadWorker.formatTag(folder.fileId)) + .addTag(FolderDownloadWorker.formatParentTag(folder.parentId)) .setInputData(data) .setConstraints(constraints) .setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) diff --git a/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadEventBroadcaster.kt b/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadEventBroadcaster.kt deleted file mode 100644 index 7bd36992902e..000000000000 --- a/app/src/main/java/com/nextcloud/client/jobs/folderDownload/FolderDownloadEventBroadcaster.kt +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2026 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package com.nextcloud.client.jobs.folderDownload - -import android.content.Context -import android.content.Intent -import androidx.localbroadcastmanager.content.LocalBroadcastManager -import com.owncloud.android.lib.common.utils.Log_OC - -class FolderDownloadEventBroadcaster( - private val context: Context, - private val broadcastManager: LocalBroadcastManager -) { - companion object { - private const val TAG = "📣" + "FolderDownloadBroadcastManager" - - private const val PREFIX = "com.nextcloud.client.folderDownload." - - const val ACTION_DOWNLOAD_ENQUEUED = PREFIX + "ACTION_DOWNLOAD_ENQUEUED" - const val ACTION_DOWNLOAD_COMPLETED = PREFIX + "ACTION_DOWNLOAD_COMPLETED" - - const val EXTRA_FILE_ID = PREFIX + "EXTRA_FILE_ID" - } - - fun sendDownloadEnqueued(id: Long) { - Log_OC.d(TAG, "Download enqueued broadcast sent") - - val intent = Intent(ACTION_DOWNLOAD_ENQUEUED).apply { - putExtra(EXTRA_FILE_ID, id) - } - - broadcastManager.sendBroadcast(intent) - } - - fun sendDownloadCompleted(id: Long) { - Log_OC.d(TAG, "Download completed broadcast sent") - - val intent = Intent(ACTION_DOWNLOAD_COMPLETED).apply { - putExtra(EXTRA_FILE_ID, id) - } - - broadcastManager.sendBroadcast(intent) - } -} 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 a3856b7e5e25..2f3cafd8f0dc 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 @@ -8,12 +8,14 @@ package com.nextcloud.client.jobs.folderDownload import android.content.Context -import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.work.CoroutineWorker import androidx.work.ForegroundInfo import androidx.work.WorkerParameters +import androidx.work.workDataOf import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.jobs.BackgroundJobManagerImpl import com.nextcloud.client.jobs.download.FileDownloadHelper +import com.nextcloud.utils.AppConstants import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.common.OwnCloudClientManagerFactory @@ -35,7 +37,6 @@ class FolderDownloadWorker( private val accountManager: UserAccountManager, private val context: Context, viewThemeUtils: ViewThemeUtils, - localBroadcastManager: LocalBroadcastManager, params: WorkerParameters ) : CoroutineWorker(context, params) { @@ -43,12 +44,17 @@ class FolderDownloadWorker( private const val TAG = "📂" + "FolderDownloadWorker" const val FOLDER_ID = "FOLDER_ID" const val ACCOUNT_NAME = "ACCOUNT_NAME" + const val FILE_ID = "FILE_ID" + const val PREVIOUS_FILE_ID = "PREVIOUS_FILE_ID" + const val IS_FOLDER_DOWNLOADED = "IS_FOLDER_DOWNLOADED" private val pendingDownloads: MutableSet = ConcurrentHashMap.newKeySet() fun isDownloading(id: Long): Boolean = pendingDownloads.contains(id) + fun formatTag(folderId: Long): String = BackgroundJobManagerImpl.JOB_DOWNLOAD_FOLDER + folderId + fun formatParentTag(parentId: Long): String = + BackgroundJobManagerImpl.JOB_DOWNLOAD_FOLDER + "_parent_" + parentId } private val notificationManager = FolderDownloadWorkerNotificationManager(context, true, viewThemeUtils) - private val folderDownloadEventBroadcaster = FolderDownloadEventBroadcaster(context, localBroadcastManager) private lateinit var storageManager: FileDataStorageManager @Suppress("ReturnCount", "DEPRECATION") @@ -82,7 +88,6 @@ class FolderDownloadWorker( Log_OC.d(TAG, "🕒 started for ${user.accountName} | folder=${folder.fileName}") trySetForeground(folder) - folderDownloadEventBroadcaster.sendDownloadEnqueued(folder.fileId) pendingDownloads.add(folder.fileId) val downloadHelper = FileDownloadHelper.instance() @@ -100,6 +105,7 @@ class FolderDownloadWorker( } var overallSuccess = true + var previousFileId: Long? = null files.forEachIndexed { index, file -> if (isStopped) { @@ -119,6 +125,7 @@ class FolderDownloadWorker( files.size ) + setProgress(workDataOf(FILE_ID to file.fileId, PREVIOUS_FILE_ID to previousFileId)) val operation = DownloadFileOperation(user, file, context) val operationResult = operation.execute(client) @@ -132,6 +139,7 @@ class FolderDownloadWorker( Log_OC.w(TAG, "⚠️ download failed for ${file.remotePath}: ${operationResult?.logMessage}") overallSuccess = false } + previousFileId = file.fileId } notificationManager.showCompletionNotification(folder.fileName, overallSuccess) @@ -148,11 +156,11 @@ class FolderDownloadWorker( notificationManager.showCompletionNotification(folder.fileName, false) Result.failure() } finally { - folderDownloadEventBroadcaster.sendDownloadCompleted(folder.fileId) pendingDownloads.remove(folder.fileId) + setProgress(workDataOf(IS_FOLDER_DOWNLOADED to true)) // delay so that user can see the error or success notification - delay(2000) + delay(AppConstants.TWO_SECOND) notificationManager.dismiss() } } @@ -187,12 +195,10 @@ class FolderDownloadWorker( } } - private fun getOCFile(operation: DownloadFileOperation): OCFile? = operation.file?.fileId?.let { - storageManager.getFileById(it) - } - ?: storageManager.getFileByDecryptedRemotePath(operation.file?.remotePath) + private fun getOCFile(operation: DownloadFileOperation): OCFile? = storageManager.getFileById(operation.file.fileId) + ?: storageManager.getFileByDecryptedRemotePath(operation.file.remotePath) ?: run { - Log_OC.e(TAG, "could not resolve OCFile for save: ${operation.file?.remotePath}") + Log_OC.e(TAG, "could not resolve OCFile for save: ${operation.file.remotePath}") null } diff --git a/app/src/main/java/com/nextcloud/utils/AppConstants.kt b/app/src/main/java/com/nextcloud/utils/AppConstants.kt index 9c41f5239531..67da73eb62a9 100644 --- a/app/src/main/java/com/nextcloud/utils/AppConstants.kt +++ b/app/src/main/java/com/nextcloud/utils/AppConstants.kt @@ -15,6 +15,7 @@ object AppConstants { const val SECONDS_PER_MINUTE = 60 const val MINUTES_PER_HOUR = 60 val ONE_SECOND = 1.seconds + val TWO_SECOND = 2.seconds const val BYTES_PER_KB = 1024 const val BYTES_PER_MB = BYTES_PER_KB * BYTES_PER_KB 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 8a8acc98942d..fc371955f0ad 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 @@ -51,7 +51,9 @@ import androidx.core.view.MenuItemCompat import androidx.core.view.ViewCompat 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 @@ -70,7 +72,6 @@ import com.nextcloud.client.files.DeepLinkHandler import com.nextcloud.client.jobs.download.FileDownloadEventBroadcaster import com.nextcloud.client.jobs.download.FileDownloadHelper import com.nextcloud.client.jobs.download.FileDownloadWorker -import com.nextcloud.client.jobs.folderDownload.FolderDownloadEventBroadcaster import com.nextcloud.client.jobs.upload.FileUploadEventBroadcaster import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.client.jobs.upload.FileUploadWorker @@ -212,9 +213,6 @@ class FileDisplayActivity : private val fileDownloadStartedReceiver = FileDownloadStartedReceiver() private val fileDownloadCompletedReceiver = FileDownloadCompletedReceiver() - private val folderDownloadStartedReceiver = FolderDownloadStartedReceiver() - private val folderDownloadCompletedReceiver = FolderDownloadCompletedReceiver() - private lateinit var albumOperationListener: AlbumOperationListener var mLastSslUntrustedServerResult: RemoteOperationResult<*>? = null @@ -292,6 +290,11 @@ class FileDisplayActivity : private lateinit var folderRefreshScheduler: FolderRefreshScheduler + override fun setFile(file: OCFile?) { + super.setFile(file) + file?.takeIf { it.isFolder }?.let { viewModel.setCurrentFolder(it.fileId) } + } + fun setFileIDForImmediatePreview(fileIDForImmediatePreview: Long) { this.fileIDForImmediatePreview = fileIDForImmediatePreview } @@ -1536,14 +1539,6 @@ class FileDisplayActivity : val uploadFinishedIntent = IntentFilter(FileUploadEventBroadcaster.ACTION_UPLOAD_COMPLETED) registerReceiver(fileUploadCompletedReceiver, uploadFinishedIntent) - val folderDownloadStartedIntentFilter = - IntentFilter(FolderDownloadEventBroadcaster.ACTION_DOWNLOAD_ENQUEUED) - registerReceiver(folderDownloadStartedReceiver, folderDownloadStartedIntentFilter) - - val folderDownloadFinishedIntentFilter = - IntentFilter(FolderDownloadEventBroadcaster.ACTION_DOWNLOAD_COMPLETED) - registerReceiver(folderDownloadCompletedReceiver, folderDownloadFinishedIntentFilter) - val fileDownloadStartedIntentFilter = IntentFilter(FileDownloadEventBroadcaster.ACTION_DOWNLOAD_ENQUEUED) registerReceiver(fileDownloadStartedReceiver, fileDownloadStartedIntentFilter) @@ -1568,8 +1563,6 @@ class FileDisplayActivity : unregisterReceiver(fileUploadCompletedReceiver) unregisterReceiver(fileDownloadStartedReceiver) unregisterReceiver(fileDownloadCompletedReceiver) - unregisterReceiver(folderDownloadStartedReceiver) - unregisterReceiver(folderDownloadCompletedReceiver) } } // endregion @@ -1876,43 +1869,13 @@ class FileDisplayActivity : ocFileListFragment?.adapter?.updateFileIndicator(state.iconId, file) } - // region FolderDownloadWorker receivers - @Suppress("ReturnCount") - private fun getFolderFromFolderDownloadWorker(intent: Intent): OCFile? { - val id = intent.getLongExtra(FolderDownloadEventBroadcaster.EXTRA_FILE_ID, -1L) - if (id == -1L) { - Log_OC.e(TAG, "invalid id received") - return null - } - - val folder = storageManager.getFileById(id) - if (folder == null) { - Log_OC.e(TAG, "folder not exists") - return null - } - - return folder + // region FolderDownloadWorker observers + private fun onFolderFileDownloadProgress(fileId: Long, previousFileId: Long?) { + val adapter = ocFileListFragment?.adapter ?: return + previousFileId?.let { storageManager.getFileById(it) }?.let(adapter::replaceFile) + adapter.updateFileIndicator(fileId, FileDownloadIndicator.Downloading.iconId) } - private inner class FolderDownloadStartedReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - Log_OC.d(TAG, "download worker started") - val folder = getFolderFromFolderDownloadWorker(intent) ?: return - updateFileDownloadIndicator(FileDownloadIndicator.Downloading, folder) - } - } - - private inner class FolderDownloadCompletedReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - Log_OC.d(TAG, "download worker finished") - val folder = getFolderFromFolderDownloadWorker(intent) ?: return - updateFileDownloadIndicator(FileDownloadIndicator.Downloaded, folder) - - if (folder.fileId == getCurrentDir()?.fileId) { - updateListOfFilesFragment() - } - } - } // endregion // region FileDownloadWorker receivers @@ -2111,6 +2074,15 @@ class FileDisplayActivity : refreshCurrentDirectory() }) } + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.observeFolderDownloadWorker( + onFileProgress = ::onFolderFileDownloadProgress, + onCompleted = ::updateListOfFilesFragment + ) + } + } } fun canPreviewInMediaPager(file: OCFile?): Boolean = diff --git a/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/FileDisplayActivityViewModel.kt b/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/FileDisplayActivityViewModel.kt index 477ab6e4f0e0..2c8b31e9424e 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/FileDisplayActivityViewModel.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/filedisplayactivity/FileDisplayActivityViewModel.kt @@ -8,14 +8,27 @@ package com.owncloud.android.ui.activity.filedisplayactivity import androidx.lifecycle.ViewModel +import androidx.work.Data import androidx.work.WorkInfo import androidx.work.WorkManager import com.nextcloud.client.jobs.BackgroundJobManagerImpl +import com.nextcloud.client.jobs.folderDownload.FolderDownloadWorker import com.nextcloud.client.jobs.offlineOperations.OfflineOperationsWorker +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.merge import javax.inject.Inject class FileDisplayActivityViewModel @Inject constructor(private val workManager: WorkManager) : ViewModel() { + private val currentFolderId = MutableStateFlow(null) + suspend fun observeOfflineWorker(onComplete: () -> Unit) { workManager .getWorkInfosByTagFlow(BackgroundJobManagerImpl.formatClassTag(OfflineOperationsWorker::class)) @@ -25,4 +38,40 @@ class FileDisplayActivityViewModel @Inject constructor(private val workManager: } } } + + fun setCurrentFolder(folderId: Long) { + currentFolderId.value = folderId + } + + @OptIn(ExperimentalCoroutinesApi::class) + suspend fun observeFolderDownloadWorker( + onFileProgress: (fileId: Long, previousFileId: Long?) -> Unit, + onCompleted: () -> Unit + ) { + currentFolderId + .filterNotNull() + .flatMapLatest { folderId -> + merge( + observeRunningProgress(FolderDownloadWorker.formatTag(folderId)), + observeRunningProgress(FolderDownloadWorker.formatParentTag(folderId)) + .filter { it.isFolderDownloaded() } + ) + } + .collect { progress -> + if (progress.isFolderDownloaded()) { + onCompleted() + return@collect + } + + val fileId = progress.keyValueMap[FolderDownloadWorker.FILE_ID] as? Long ?: return@collect + onFileProgress(fileId, progress.keyValueMap[FolderDownloadWorker.PREVIOUS_FILE_ID] as? Long) + } + } + + private fun observeRunningProgress(tag: String): Flow = workManager + .getWorkInfosByTagFlow(tag) + .mapNotNull { workInfos -> workInfos.firstOrNull { it.state == WorkInfo.State.RUNNING }?.progress } + .distinctUntilChanged() + + private fun Data.isFolderDownloaded(): Boolean = getBoolean(FolderDownloadWorker.IS_FOLDER_DOWNLOADED, false) } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/filelist/OCFileListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/filelist/OCFileListAdapter.kt index ffc92edafda6..2937739aa7af 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/filelist/OCFileListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/filelist/OCFileListAdapter.kt @@ -555,6 +555,13 @@ class OCFileListAdapter( file?.let { notifyItemChangedIfPresent(it, iconId) } } + fun updateFileIndicator(fileId: Long, iconId: Int) { + val index = files.indexOfFirst { it.fileId == fileId } + if (index != RecyclerView.NO_POSITION) { + notifyItemChanged(index + differ.headerOffset(), iconId) + } + } + @SuppressLint("NotifyDataSetChanged") fun updateRecommendedFiles(value: List) { recommendedFiles.clear()