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..958db34e5a02 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/FolderRefreshScheduler.kt @@ -0,0 +1,101 @@ +/* + * 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.isActive +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 (isActive) { + delay(ETAG_POLL_INTERVAL_MS) + checkAndRefreshIfETagChanged() + } + } + + Log_OC.d(TAG, "eTag polling started interval 30 seconds") + } + + 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()) { + Log_OC.w(TAG, "activity is finished or search is opened") + return + } + + val currentDir = activity.getCurrentDir() + if (currentDir == null) { + Log_OC.w(TAG, "current directory is null") + return + } + + val currentUser = activity.user.orElse(null) + if (currentUser == null) { + Log_OC.w(TAG, "current user is 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, 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") + 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() }