From 94981c2c7815911244c7fdf5865d7765bb1a3f80 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 10 Mar 2026 12:44:06 +0100 Subject: [PATCH 1/3] feat(file-list): folder refresh scheduler Signed-off-by: alperozturk96 --- .../operations/FolderRefreshScheduler.kt | 91 +++++++++++++++++++ .../ui/activity/FileDisplayActivity.kt | 9 +- 2 files changed, 99 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/owncloud/android/operations/FolderRefreshScheduler.kt diff --git a/app/src/main/java/com/owncloud/android/operations/FolderRefreshScheduler.kt b/app/src/main/java/com/owncloud/android/operations/FolderRefreshScheduler.kt new file mode 100644 index 000000000000..cc821c462ef4 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/FolderRefreshScheduler.kt @@ -0,0 +1,91 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.operations + +import androidx.lifecycle.lifecycleScope +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.files.CheckEtagRemoteOperation +import com.owncloud.android.ui.activity.FileDisplayActivity +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class FolderRefreshScheduler(private val activity: FileDisplayActivity) { + companion object { + private const val ETAG_POLL_INTERVAL_MS = 30_000L + private const val TAG = "FolderRefreshScheduler" + } + + private var job: Job? = null + + fun start() { + stop() + + job = activity.lifecycleScope.launch { + while (true) { + delay(ETAG_POLL_INTERVAL_MS) + checkAndRefreshIfETagChanged() + } + } + + Log_OC.d(TAG, "ETag polling started (interval=${ETAG_POLL_INTERVAL_MS}ms)") + } + + fun stop() { + job?.cancel() + job = null + Log_OC.d(TAG, "ETag polling stopped") + } + + @Suppress("ReturnCount", "TooGenericExceptionCaught") + private suspend fun checkAndRefreshIfETagChanged() { + if (activity.isFinishing || activity.isSearchOpen()) return + + val fragment = activity.listOfFilesFragment ?: return + if (fragment.isSearchFragment) return + + val currentDir = activity.getCurrentDir() ?: return + val currentUser = activity.user.orElse(null) ?: return + + val localEtag = currentDir.etag ?: "" + + Log_OC.d(TAG, "eTag poll → checking '${currentDir.remotePath}' (local eTag='$localEtag')") + + val result = withContext(Dispatchers.IO) { + try { + CheckEtagRemoteOperation(currentDir.remotePath, localEtag).execute(currentUser, activity) + } catch (e: Exception) { + Log_OC.e(TAG, "eTag poll: could not create client — ${e.message}") + null + } + } ?: return + + when (result.code) { + RemoteOperationResult.ResultCode.ETAG_CHANGED -> { + Log_OC.i(TAG, "eTag poll → eTag changed for '${currentDir.remotePath}', triggering sync") + activity.startSyncFolderOperation(currentDir, ignoreETag = true) + } + + RemoteOperationResult.ResultCode.ETAG_UNCHANGED -> { + Log_OC.d(TAG, "eTag poll → no change for '${currentDir.remotePath}'") + } + + RemoteOperationResult.ResultCode.FILE_NOT_FOUND -> { + Log_OC.w(TAG, "eTag poll → directory not found on server, refreshing to handle deletion") + activity.startSyncFolderOperation(currentDir, ignoreETag = true) + } + + else -> { + Log_OC.w(TAG, "eTag poll → unexpected result code: ${result.code}") + } + } + } +} 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 21a74acb64bf..3b601d2375c7 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 @@ -105,6 +105,7 @@ import com.owncloud.android.lib.resources.notifications.GetNotificationsRemoteOp import com.owncloud.android.operations.CopyFileOperation import com.owncloud.android.operations.CreateFolderOperation import com.owncloud.android.operations.DownloadType +import com.owncloud.android.operations.FolderRefreshScheduler import com.owncloud.android.operations.MoveFileOperation import com.owncloud.android.operations.RefreshFolderOperation import com.owncloud.android.operations.RemoveFileOperation @@ -250,6 +251,8 @@ class FileDisplayActivity : */ private var fileIDForImmediatePreview: Long = -1 + private lateinit var folderRefreshScheduler: FolderRefreshScheduler + fun setFileIDForImmediatePreview(fileIDForImmediatePreview: Long) { this.fileIDForImmediatePreview = fileIDForImmediatePreview } @@ -262,6 +265,7 @@ class FileDisplayActivity : super.onCreate(savedInstanceState) lastDisplayedAccountName = preferences.lastDisplayedAccountName + folderRefreshScheduler = FolderRefreshScheduler(this) intent?.let { handleCommonIntents(it) @@ -1164,7 +1168,7 @@ class FileDisplayActivity : uploader.uploadUris() } - private fun isSearchOpen(): Boolean { + fun isSearchOpen(): Boolean { if (searchView == null) { return false } else { @@ -1350,6 +1354,8 @@ class FileDisplayActivity : super.onResume() + folderRefreshScheduler.start() + if (ocFileListFragment?.isSearchFragment == true) { ocFileListFragment?.setSearchArgs(ocFileListFragment?.arguments) } @@ -1476,6 +1482,7 @@ class FileDisplayActivity : override fun onStop() { Log_OC.v(TAG, "onStop()") + folderRefreshScheduler.stop() unregisterReceivers() super.onStop() } From dda79dd52270e8c330d2345654ccdcd0f9d252ac Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 10 Mar 2026 12:47:22 +0100 Subject: [PATCH 2/3] feat(file-list): folder refresh scheduler Signed-off-by: alperozturk96 --- .../com/owncloud/android/operations/FolderRefreshScheduler.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/operations/FolderRefreshScheduler.kt b/app/src/main/java/com/owncloud/android/operations/FolderRefreshScheduler.kt index cc821c462ef4..62783c13bf57 100644 --- a/app/src/main/java/com/owncloud/android/operations/FolderRefreshScheduler.kt +++ b/app/src/main/java/com/owncloud/android/operations/FolderRefreshScheduler.kt @@ -63,7 +63,7 @@ class FolderRefreshScheduler(private val activity: FileDisplayActivity) { try { CheckEtagRemoteOperation(currentDir.remotePath, localEtag).execute(currentUser, activity) } catch (e: Exception) { - Log_OC.e(TAG, "eTag poll: could not create client — ${e.message}") + Log_OC.e(TAG, e.message) null } } ?: return @@ -79,7 +79,7 @@ class FolderRefreshScheduler(private val activity: FileDisplayActivity) { } RemoteOperationResult.ResultCode.FILE_NOT_FOUND -> { - Log_OC.w(TAG, "eTag poll → directory not found on server, refreshing to handle deletion") + Log_OC.w(TAG, "eTag poll → directory not found on server") activity.startSyncFolderOperation(currentDir, ignoreETag = true) } From 50a57ad7eae50d90de2c4c2acc0e5d88e228f8b2 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 11 Mar 2026 09:53:20 +0100 Subject: [PATCH 3/3] fix current dir Signed-off-by: alperozturk96 --- .../operations/FolderRefreshScheduler.kt | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/operations/FolderRefreshScheduler.kt b/app/src/main/java/com/owncloud/android/operations/FolderRefreshScheduler.kt index 62783c13bf57..958db34e5a02 100644 --- a/app/src/main/java/com/owncloud/android/operations/FolderRefreshScheduler.kt +++ b/app/src/main/java/com/owncloud/android/operations/FolderRefreshScheduler.kt @@ -15,6 +15,7 @@ import com.owncloud.android.ui.activity.FileDisplayActivity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -30,30 +31,39 @@ class FolderRefreshScheduler(private val activity: FileDisplayActivity) { stop() job = activity.lifecycleScope.launch { - while (true) { + while (isActive) { delay(ETAG_POLL_INTERVAL_MS) checkAndRefreshIfETagChanged() } } - Log_OC.d(TAG, "ETag polling started (interval=${ETAG_POLL_INTERVAL_MS}ms)") + Log_OC.d(TAG, "eTag polling started interval 30 seconds") } fun stop() { job?.cancel() job = null - Log_OC.d(TAG, "ETag polling stopped") + Log_OC.d(TAG, "eTag polling stopped") } @Suppress("ReturnCount", "TooGenericExceptionCaught") private suspend fun checkAndRefreshIfETagChanged() { - if (activity.isFinishing || activity.isSearchOpen()) return + if (activity.isFinishing || activity.isSearchOpen()) { + Log_OC.w(TAG, "activity is finished or search is opened") + return + } - val fragment = activity.listOfFilesFragment ?: return - if (fragment.isSearchFragment) return + val currentDir = activity.getCurrentDir() + if (currentDir == null) { + Log_OC.w(TAG, "current directory is null") + return + } - val currentDir = activity.getCurrentDir() ?: return - val currentUser = activity.user.orElse(null) ?: return + val currentUser = activity.user.orElse(null) + if (currentUser == null) { + Log_OC.w(TAG, "current user is null") + return + } val localEtag = currentDir.etag ?: ""