From 5811460bd8c2723d060dffdb24fcb63b3fa17942 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 9 Apr 2026 11:16:35 +0200 Subject: [PATCH 1/6] fix(upload-list): handle conflict actions Signed-off-by: alperozturk96 --- .../client/jobs/upload/FileUploadHelper.kt | 17 +- .../utils/UploadErrorNotificationManager.kt | 3 +- .../utils/extensions/RemoteFileExtensions.kt | 26 +++ .../extensions/UploadResultExtensions.kt | 2 +- .../datamodel/UploadsStorageManager.kt | 3 +- .../operations/UploadFileOperation.java | 9 +- .../android/ui/activity/UploadListActivity.kt | 88 ++++++++-- .../adapter/uploadList/UploadListAdapter.kt | 166 ++++++------------ .../helper/ConflictHandlingResult.kt | 17 ++ .../helper/UploadListAdapterAction.kt | 20 +++ .../helper/UploadListAdapterActionHandler.kt | 71 ++++++++ .../{ => helper}/UploadListAdapterHelper.kt | 10 +- .../helper/UploadListItemOnClick.kt | 14 ++ .../{ => model}/UploadListSection.kt | 2 +- .../uploadList/{ => model}/UploadListType.kt | 2 +- app/src/main/res/values/strings.xml | 7 +- 16 files changed, 296 insertions(+), 161 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/utils/extensions/RemoteFileExtensions.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/ConflictHandlingResult.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListAdapterAction.kt create mode 100644 app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListAdapterActionHandler.kt rename app/src/main/java/com/owncloud/android/ui/adapter/uploadList/{ => helper}/UploadListAdapterHelper.kt (88%) create mode 100644 app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListItemOnClick.kt rename app/src/main/java/com/owncloud/android/ui/adapter/uploadList/{ => model}/UploadListSection.kt (97%) rename app/src/main/java/com/owncloud/android/ui/adapter/uploadList/{ => model}/UploadListType.kt (80%) diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt index b0ec486822e3..579b534a21d3 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt @@ -26,6 +26,7 @@ import com.nextcloud.client.notifications.AppWideNotificationManager import com.nextcloud.utils.extensions.checkWCFRestrictions import com.nextcloud.utils.extensions.getUploadIds import com.nextcloud.utils.extensions.isLastResultConflictError +import com.nextcloud.utils.extensions.isSame import com.owncloud.android.MainApp import com.owncloud.android.R import com.owncloud.android.datamodel.FileDataStorageManager @@ -46,7 +47,6 @@ import com.owncloud.android.lib.resources.status.OCCapability import com.owncloud.android.operations.RemoveFileOperation import com.owncloud.android.operations.UploadFileOperation import com.owncloud.android.utils.DisplayUtils -import com.owncloud.android.utils.FileUtil import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -563,25 +563,17 @@ class FileUploadHelper { } @Suppress("MagicNumber", "ReturnCount", "ComplexCondition") - fun isSameFileOnRemote(user: User?, localFile: File?, remotePath: String?, context: Context?): Boolean { - if (user == null || localFile == null || remotePath == null || context == null) { + fun isSameFileOnRemote(user: User?, localPath: String?, remotePath: String?, context: Context?): Boolean { + if (user == null || localPath == null || remotePath == null || context == null) { Log_OC.e(TAG, "cannot compare remote and local file") return false } - // Compare remote file to local file - val localLastModifiedTimestamp = localFile.lastModified() / 1000 // remote file timestamp in milli not micro sec - val localCreationTimestamp = FileUtil.getCreationTimestamp(localFile) - val localSize: Long = localFile.length() - val operation = ReadFileRemoteOperation(remotePath) val result: RemoteOperationResult<*> = operation.execute(user, context) if (result.isSuccess) { val remoteFile = result.data[0] as RemoteFile - return remoteFile.size == localSize && - localCreationTimestamp != null && - localCreationTimestamp == remoteFile.creationTimestamp && - remoteFile.modifiedTimestamp == localLastModifiedTimestamp * 1000 + return remoteFile.isSame(localPath) } return false } @@ -648,7 +640,6 @@ class FileUploadHelper { files: List, accountName: String ): Pair, List> { - val autoUploadFolders = mutableListOf() val nonAutoUploadFiles = mutableListOf() diff --git a/app/src/main/java/com/nextcloud/client/jobs/utils/UploadErrorNotificationManager.kt b/app/src/main/java/com/nextcloud/client/jobs/utils/UploadErrorNotificationManager.kt index c2e24785798d..409e66c0c9fd 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/utils/UploadErrorNotificationManager.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/utils/UploadErrorNotificationManager.kt @@ -28,7 +28,6 @@ import com.owncloud.android.ui.activity.ConflictsResolveActivity import com.owncloud.android.utils.ErrorMessageAdapter import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import java.io.File object UploadErrorNotificationManager { private const val TAG = "UploadErrorNotificationManager" @@ -76,7 +75,7 @@ object UploadErrorNotificationManager { val isSameFile = withContext(Dispatchers.IO) { FileUploadHelper.instance().isSameFileOnRemote( operation.user, - File(operation.storagePath), + operation.storagePath, operation.remotePath, context ) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/RemoteFileExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/RemoteFileExtensions.kt new file mode 100644 index 000000000000..976ce99e51bc --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/RemoteFileExtensions.kt @@ -0,0 +1,26 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import com.nextcloud.utils.TimeConstants +import com.owncloud.android.lib.resources.files.model.RemoteFile +import com.owncloud.android.utils.FileUtil + +fun RemoteFile.isSame(path: String?): Boolean { + val localFile = path?.toFile() ?: return false + + // remote file timestamp in milli not micro sec + val localLastModifiedTimestamp = localFile.lastModified() / TimeConstants.MILLIS_PER_SECOND + val localCreationTimestamp = FileUtil.getCreationTimestamp(localFile) + val localSize: Long = localFile.length() + + return size == localSize && + localCreationTimestamp != null && + localCreationTimestamp == creationTimestamp && + modifiedTimestamp == localLastModifiedTimestamp * TimeConstants.MILLIS_PER_SECOND +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/UploadResultExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/UploadResultExtensions.kt index 38614ed7dc45..4ca297751522 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/UploadResultExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/UploadResultExtensions.kt @@ -94,7 +94,7 @@ fun UploadResult.getFailedStatusText(context: Context): String = when (this) { UploadResult.OLD_ANDROID_API -> context.getString(R.string.upload_old_android) - UploadResult.SYNC_CONFLICT -> context.getString(R.string.upload_sync_conflict) + UploadResult.SYNC_CONFLICT -> context.getString(R.string.upload_sync_conflict_check) UploadResult.CANNOT_CREATE_FILE -> context.getString(R.string.upload_cannot_create_file) diff --git a/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.kt b/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.kt index cd969f884689..73be317b47b5 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.kt @@ -41,7 +41,6 @@ import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.status.OCCapability import com.owncloud.android.operations.UploadFileOperation import com.owncloud.android.utils.theme.CapabilityUtils -import java.io.File import java.util.Calendar import java.util.Locale import java.util.Observable @@ -485,7 +484,7 @@ class UploadsStorageManager( } else if (code.isConflict()) { val isSame = FileUploadHelper().isSameFileOnRemote( upload.user, - File(upload.storagePath), + upload.storagePath, upload.remotePath, upload.context ) diff --git a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java index f966de291d7d..3e5cfcfa101d 100644 --- a/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -1298,13 +1298,8 @@ private RemoteOperationResult checkNameCollision(OCFile parentFile, // check if its real SYNC_CONFLICT boolean isSameFileOnRemote = false; if (mFile != null) { - String localPath = mFile.getStoragePath(); - - if (localPath != null) { - File localFile = new File(localPath); - isSameFileOnRemote = FileUploadHelper.Companion.instance() - .isSameFileOnRemote(user, localFile, mRemotePath, mContext); - } + isSameFileOnRemote = FileUploadHelper.Companion.instance() + .isSameFileOnRemote(user, mFile.getStoragePath(), mRemotePath, mContext); } if (isSameFileOnRemote) { diff --git a/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt index 9e6db3e327a9..9437e52dac53 100755 --- a/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt @@ -16,9 +16,11 @@ import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.View +import androidx.lifecycle.lifecycleScope import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.recyclerview.widget.GridLayoutManager import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.google.android.material.snackbar.Snackbar import com.nextcloud.client.account.User import com.nextcloud.client.core.Clock import com.nextcloud.client.device.PowerManagementService @@ -30,17 +32,29 @@ import com.owncloud.android.databinding.UploadListLayoutBinding import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.db.OCUpload import com.owncloud.android.lib.common.operations.RemoteOperation import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.CheckCurrentCredentialsOperation import com.owncloud.android.ui.adapter.uploadList.UploadListAdapter +import com.owncloud.android.ui.adapter.uploadList.helper.ConflictHandlingResult +import com.owncloud.android.ui.adapter.uploadList.helper.UploadListAdapterAction +import com.owncloud.android.ui.adapter.uploadList.helper.UploadListAdapterActionHandler +import com.owncloud.android.ui.adapter.uploadList.helper.UploadListAdapterHelper +import com.owncloud.android.ui.adapter.uploadList.helper.UploadListItemOnClick import com.owncloud.android.ui.decoration.MediaGridItemDecoration +import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.FilesSyncHelper +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import javax.inject.Inject @Suppress("MagicNumber") -class UploadListActivity : FileActivity() { +class UploadListActivity : + FileActivity(), + UploadListItemOnClick { @Inject lateinit var uploadsStorageManager: UploadsStorageManager @Inject lateinit var powerManagementService: PowerManagementService @@ -55,8 +69,11 @@ class UploadListActivity : FileActivity() { private var swipeListRefreshLayout: SwipeRefreshLayout? = null private var binding: UploadListLayoutBinding? = null - private var uploadListAdapter: UploadListAdapter? = null + private var uploadFinishReceiver: UploadFinishReceiver? = null + private lateinit var uploadListAdapter: UploadListAdapter + private lateinit var adapterActionHandler: UploadListAdapterAction + private lateinit var adapterHelper: UploadListAdapterHelper override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -82,19 +99,21 @@ class UploadListActivity : FileActivity() { private fun setupContent() { setupEmptyList() + adapterActionHandler = UploadListAdapterActionHandler() + adapterHelper = UploadListAdapterHelper(this) uploadListAdapter = UploadListAdapter( this, uploadsStorageManager, - storageManager, userAccountManager, connectivityService, powerManagementService, - clock, - viewThemeUtils + viewThemeUtils, + this, + adapterHelper ) val lm = GridLayoutManager(this, 1) - uploadListAdapter?.setLayoutManager(lm) + uploadListAdapter.setLayoutManager(lm) val spacing = getResources().getDimensionPixelSize(R.dimen.media_grid_spacing) binding?.list?.run { @@ -133,7 +152,7 @@ class UploadListActivity : FileActivity() { private fun loadItems() { swipeListRefreshLayout?.isRefreshing = true - uploadListAdapter?.loadUploadItemsFromDb { swipeListRefreshLayout?.isRefreshing = false } + uploadListAdapter.loadUploadItemsFromDb { swipeListRefreshLayout?.isRefreshing = false } } private fun refresh() { @@ -145,7 +164,7 @@ class UploadListActivity : FileActivity() { ) if (!isUploadStarted) { - uploadListAdapter?.loadUploadItemsFromDb { swipeListRefreshLayout?.isRefreshing = false } + uploadListAdapter.loadUploadItemsFromDb { swipeListRefreshLayout?.isRefreshing = false } } } @@ -203,7 +222,7 @@ class UploadListActivity : FileActivity() { val ids = uploadsStorageManager.getCurrentUploadIds(user.accountName) uploadHelper.cancelAndRestartUploadJob(user, ids) } - uploadListAdapter?.notifyDataSetChanged() + uploadListAdapter.notifyDataSetChanged() } override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { @@ -240,7 +259,7 @@ class UploadListActivity : FileActivity() { fileOperationsHelper.opIdWaitingFor = Long.MAX_VALUE dismissLoadingDialog() - val account = result.getData()[0] as? Account + val account = result.data[0] as? Account if (!result.isSuccess) { requestCredentialsUpdate(account) } else { @@ -253,9 +272,56 @@ class UploadListActivity : FileActivity() { } } + override fun onLastUploadResultConflictClick(upload: OCUpload) { + DisplayUtils.showSnackMessage(this, R.string.upload_sync_conflict_checking) + + lifecycleScope.launch { + val client = clientRepository.getOwncloudClient() ?: return@launch + val result = adapterActionHandler.handleConflict(upload, client, uploadsStorageManager) + + withContext(Dispatchers.Main) { + when (result) { + is ConflictHandlingResult.ConflictNotExists -> { + uploadListAdapter.notifyUploadChanged(upload) + onConflictNotExists(upload) + } + + is ConflictHandlingResult.CannotCheckConflict -> { + DisplayUtils.showSnackMessage( + this@UploadListActivity, + R.string.upload_sync_conflict_check_error + ) + } + + is ConflictHandlingResult.ShowConflictResolveDialog -> { + adapterHelper.openConflictActivity(result.file, result.upload) + } + } + } + } + } + + private fun onConflictNotExists(upload: OCUpload) { + val rootView = binding?.root ?: return + val snackbar = Snackbar.make( + rootView, + R.string.upload_sync_conflict_not_exists, + Snackbar.LENGTH_LONG + ) + + snackbar.setAction(R.string.retry) { + val optionalUser = userAccountManager.getUser(upload.accountName) + if (optionalUser.isPresent) { + FileUploadHelper.instance().retryUpload(upload, optionalUser.get()) + } + } + + snackbar.show() + } + private inner class UploadFinishReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { - throttler.run("update_upload_list") { uploadListAdapter?.loadUploadItemsFromDb() } + throttler.run("update_upload_list") { uploadListAdapter.loadUploadItemsFromDb() } } } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt index ef9347912ddd..558ac721a3f3 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt @@ -10,6 +10,7 @@ package com.owncloud.android.ui.adapter.uploadList import android.annotation.SuppressLint import android.app.NotificationManager import android.content.Context +import android.os.Looper import android.text.format.DateUtils import android.view.LayoutInflater import android.view.View @@ -21,7 +22,6 @@ import com.afollestad.sectionedrecyclerview.SectionedRecyclerViewAdapter import com.afollestad.sectionedrecyclerview.SectionedViewHolder import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager -import com.nextcloud.client.core.Clock import com.nextcloud.client.device.PowerManagementService import com.nextcloud.client.jobs.upload.FileUploadHelper import com.nextcloud.client.jobs.upload.FileUploadWorker @@ -40,13 +40,13 @@ import com.owncloud.android.datamodel.ThumbnailsCacheManager import com.owncloud.android.datamodel.UploadsStorageManager import com.owncloud.android.db.OCUpload import com.owncloud.android.db.UploadResult -import com.owncloud.android.lib.common.operations.OnRemoteOperationListener -import com.owncloud.android.lib.common.operations.RemoteOperation -import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC -import com.owncloud.android.operations.RefreshFolderOperation import com.owncloud.android.ui.activity.FileActivity import com.owncloud.android.ui.adapter.progressListener.UploadProgressListener +import com.owncloud.android.ui.adapter.uploadList.helper.UploadListAdapterHelper +import com.owncloud.android.ui.adapter.uploadList.helper.UploadListItemOnClick +import com.owncloud.android.ui.adapter.uploadList.model.UploadListSection +import com.owncloud.android.ui.adapter.uploadList.model.UploadListType import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.MimeTypeUtil import com.owncloud.android.utils.theme.ViewThemeUtils @@ -69,12 +69,12 @@ import java.util.function.Consumer class UploadListAdapter( private val activity: FileActivity, private val uploadsStorageManager: UploadsStorageManager, - private val storageManager: FileDataStorageManager, private val accountManager: UserAccountManager, private val connectivityService: ConnectivityService, private val powerManagementService: PowerManagementService, - private val clock: Clock, - private val viewThemeUtils: ViewThemeUtils + private val viewThemeUtils: ViewThemeUtils, + private val itemOnClick: UploadListItemOnClick, + private val helper: UploadListAdapterHelper ) : SectionedRecyclerViewAdapter() { private val uploadListSections = UploadListSection.sections() @@ -82,7 +82,6 @@ class UploadListAdapter( private val uploadHelper = FileUploadHelper.instance() private var uploadProgressListener: UploadProgressListener? = null private var notificationManager: NotificationManager? = null - private val helper = UploadListAdapterHelper(activity) init { Log_OC.d(TAG, "UploadListAdapter") @@ -390,11 +389,6 @@ class UploadListAdapter( private fun bindItemActions(holder: ItemViewHolder, item: OCUpload) { holder.binding.run { val optionalUser = accountManager.getUser(item.accountName) - val status = item.getStatusText( - activity, - activity.appPreferences.isGlobalUploadPaused, - uploadHelper.isUploadingNow(item) - ) // Right-side button when (item.uploadStatus) { @@ -422,9 +416,7 @@ class UploadListAdapter( if (item.isLastResultConflictError()) { setImageResource(R.drawable.ic_dots_vertical) setOnClickListener { view -> - optionalUser.ifPresent { user -> - showItemConflictPopup(user, holder, item, status, view) - } + showItemConflictPopup(item, view) } } else { setImageResource(R.drawable.ic_action_delete_grey) @@ -444,7 +436,7 @@ class UploadListAdapter( UploadsStorageManager.UploadStatus.UPLOAD_FAILED, UploadsStorageManager.UploadStatus.UPLOAD_CANCELLED -> setOnClickListener { - onFailedOrCancelledItemClick(item, optionalUser, holder, status) + onFailedOrCancelledItemClick(item, optionalUser) } UploadsStorageManager.UploadStatus.UPLOAD_SUCCEEDED -> @@ -461,12 +453,7 @@ class UploadListAdapter( } } - private fun onFailedOrCancelledItemClick( - item: OCUpload, - optionalUser: Optional, - holder: ItemViewHolder, - status: String - ) { + private fun onFailedOrCancelledItemClick(item: OCUpload, optionalUser: Optional) { if (optionalUser.isEmpty) { return } @@ -475,10 +462,7 @@ class UploadListAdapter( if (item.lastResult == UploadResult.CREDENTIAL_ERROR) { activity.fileOperationsHelper.checkCurrentCredentials(user) } else if (item.isLastResultConflictError()) { - if (checkAndOpenConflictResolutionDialog(user, holder, item, status)) { - return - } - retryOrShowError(item) + itemOnClick.onLastUploadResultConflictClick(item) } else { retryOrShowError(item) } @@ -611,78 +595,12 @@ class UploadListAdapter( itemViewHolder.binding.thumbnail.setImageDrawable(drawable) } - private fun checkAndOpenConflictResolutionDialog( - user: User?, - itemViewHolder: ItemViewHolder, - item: OCUpload, - status: String? - ): Boolean { - val remotePath = item.remotePath - val localFile = storageManager.getFileByEncryptedRemotePath(remotePath) - - if (localFile == null) { - // Remote file doesn't exist, try to refresh folder - val folder = storageManager.getFileByEncryptedRemotePath(File(remotePath).getParent() + "/") - - if (folder != null && folder.isFolder) { - refreshFolderAndUpdateUI(itemViewHolder, user, folder, remotePath, item, status) - return true - } - - // Destination folder doesn't exist anymore - } - - if (localFile != null) { - helper.openConflictActivity(localFile, item) - return true - } - - // Remote file doesn't exist anymore = there is no more conflict - return false - } - - private fun refreshFolderAndUpdateUI( - holder: ItemViewHolder, - user: User?, - folder: OCFile?, - remotePath: String?, - item: OCUpload, - status: String? - ) { - refreshFolder( - holder, - user, - folder - ) { _: RemoteOperation<*>?, result: RemoteOperationResult<*>? -> - holder.binding.uploadStatus.text = status - if (result?.isSuccess == true) { - val fileOnServer = storageManager.getFileByEncryptedRemotePath(remotePath) - if (fileOnServer != null) { - helper.openConflictActivity(fileOnServer, item) - } else { - displayFileNotFoundError(holder.itemView, activity) - } - } - } - } - - private fun displayFileNotFoundError(itemView: View?, context: Context) { - val message = context.getString(R.string.uploader_file_not_found_message) - DisplayUtils.showSnackMessage(itemView, message) - } - - private fun showItemConflictPopup( - user: User?, - holder: ItemViewHolder, - item: OCUpload, - status: String?, - view: View? - ) { + private fun showItemConflictPopup(item: OCUpload, view: View) { PopupMenu(activity, view).apply { inflate(R.menu.upload_list_item_file_conflict) setOnMenuItemClickListener { menuItem -> if (menuItem.itemId == R.id.action_upload_list_resolve_conflict) { - checkAndOpenConflictResolutionDialog(user, holder, item, status) + itemOnClick.onLastUploadResultConflictClick(item) } else { removeUpload(item) } @@ -698,25 +616,6 @@ class UploadListAdapter( loadUploadItemsFromDb() } - private fun refreshFolder(view: ItemViewHolder, user: User?, folder: OCFile?, listener: OnRemoteOperationListener) { - view.binding.uploadListItemLayout.isClickable = false - view.binding.uploadStatus.setText(R.string.uploads_view_upload_status_fetching_server_version) - RefreshFolderOperation( - folder, - clock.currentTime, - false, - false, - true, - storageManager, - user, - activity - ) - .execute(user, activity, { caller, result -> - view.binding.uploadListItemLayout.isClickable = true - listener.onRemoteOperationFinish(caller, result) - }, activity.handler) - } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SectionedViewHolder = if (viewType == VIEW_TYPE_HEADER) { HeaderViewHolder( @@ -772,6 +671,43 @@ class UploadListAdapter( notificationManager?.cancel(upload.uploadId.toInt()) } + fun notifyUploadChanged(upload: OCUpload) { + for (sectionIndex in uploadListSections.indices) { + val section = uploadListSections[sectionIndex] + + val itemIndex = section.items.indexOfFirst { it.uploadId == upload.uploadId } + + if (itemIndex != -1) { + val adapterPosition = getAdapterPosition(sectionIndex, itemIndex) + + if (adapterPosition != -1) { + val updatedItems = section.items.toMutableList().apply { + this[itemIndex] = upload + } + + uploadListSections[sectionIndex] = section.withItems(updatedItems) + + if (Looper.myLooper() == Looper.getMainLooper()) { + notifyItemChanged(adapterPosition) + } else { + activity.runOnUiThread { + notifyItemChanged(adapterPosition) + } + } + } + return + } + } + } + + private fun getAdapterPosition(section: Int, relativePosition: Int): Int { + var position = 0 + for (i in 0 until section) { + position += uploadListSections[i].items.size + 1 + } + return position + 1 + relativePosition + } + companion object { private val TAG: String = UploadListAdapter::class.java.getSimpleName() } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/ConflictHandlingResult.kt b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/ConflictHandlingResult.kt new file mode 100644 index 000000000000..ff0d07d7c99a --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/ConflictHandlingResult.kt @@ -0,0 +1,17 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.adapter.uploadList.helper + +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.db.OCUpload + +sealed class ConflictHandlingResult { + data object CannotCheckConflict : ConflictHandlingResult() + data object ConflictNotExists : ConflictHandlingResult() + data class ShowConflictResolveDialog(val file: OCFile, val upload: OCUpload) : ConflictHandlingResult() +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListAdapterAction.kt b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListAdapterAction.kt new file mode 100644 index 000000000000..0ff6128edca9 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListAdapterAction.kt @@ -0,0 +1,20 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.adapter.uploadList.helper + +import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.db.OCUpload +import com.owncloud.android.lib.common.OwnCloudClient + +interface UploadListAdapterAction { + suspend fun handleConflict( + upload: OCUpload, + client: OwnCloudClient, + storageManager: UploadsStorageManager + ): ConflictHandlingResult +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListAdapterActionHandler.kt b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListAdapterActionHandler.kt new file mode 100644 index 000000000000..d8590b6db9fe --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListAdapterActionHandler.kt @@ -0,0 +1,71 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.adapter.uploadList.helper + +import com.nextcloud.utils.extensions.isSame +import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.db.OCUpload +import com.owncloud.android.db.UploadResult +import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.operations.RemoteOperationResult +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation +import com.owncloud.android.lib.resources.files.model.RemoteFile +import com.owncloud.android.utils.FileStorageUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class UploadListAdapterActionHandler : UploadListAdapterAction { + companion object { + private const val TAG = "UploadListAdapterActionHandler" + } + + override suspend fun handleConflict( + upload: OCUpload, + client: OwnCloudClient, + storageManager: UploadsStorageManager + ): ConflictHandlingResult = withContext(Dispatchers.IO) { + val operationResult = ReadFileRemoteOperation(upload.remotePath).execute(client) + + if (!operationResult.isSuccess) { + return@withContext when (operationResult.code) { + RemoteOperationResult.ResultCode.FILE_NOT_FOUND -> { + onConflictNotExists(upload, storageManager) + } + + else -> { + Log_OC.e(TAG, "cannot check conflict, operation result is not success") + ConflictHandlingResult.CannotCheckConflict + } + } + } + + val remoteFile = operationResult.data[0] as? RemoteFile + ?: run { + Log_OC.e(TAG, "cannot check conflict, operation result cannot be cast to RemoteFile") + return@withContext ConflictHandlingResult.CannotCheckConflict + } + + val ocFile = FileStorageUtils.fillOCFile(remoteFile) + + if (remoteFile.isSame(ocFile.storagePath)) { + onConflictNotExists(upload, storageManager) + } else { + ConflictHandlingResult.ShowConflictResolveDialog(ocFile, upload) + } + } + + private fun onConflictNotExists( + upload: OCUpload, + storageManager: UploadsStorageManager + ): ConflictHandlingResult.ConflictNotExists { + upload.lastResult = UploadResult.UNKNOWN + storageManager.updateUpload(upload) + return ConflictHandlingResult.ConflictNotExists + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapterHelper.kt b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListAdapterHelper.kt similarity index 88% rename from app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapterHelper.kt rename to app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListAdapterHelper.kt index 64cd8edb56c0..63af435a21fe 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapterHelper.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListAdapterHelper.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.owncloud.android.ui.adapter.uploadList +package com.owncloud.android.ui.adapter.uploadList.helper import android.content.ActivityNotFoundException import android.content.Intent @@ -32,7 +32,7 @@ class UploadListAdapterHelper(private val activity: FileActivity) { file.setStoragePath(upload.localPath) val user = activity.accountManager.getUser(upload.accountName) if (user.isPresent) { - val intent = ConflictsResolveActivity.createIntent( + val intent = ConflictsResolveActivity.Companion.createIntent( file, user.get(), upload.uploadId, @@ -61,16 +61,16 @@ class UploadListAdapterHelper(private val activity: FileActivity) { } val optionalUser = activity.user - if (PreviewImageFragment.canBePreviewed(file) && optionalUser.isPresent) { + if (PreviewImageFragment.Companion.canBePreviewed(file) && optionalUser.isPresent) { // show image preview and stay in uploads tab - val intent = FileDisplayActivity.openFileIntent(activity, optionalUser.get(), file) + val intent = FileDisplayActivity.Companion.openFileIntent(activity, optionalUser.get(), file) activity.startActivity(intent) return } val intent = Intent(activity, FileDisplayActivity::class.java).apply { setAction(Intent.ACTION_VIEW) - putExtra(FileDisplayActivity.KEY_FILE_PATH, upload.remotePath) + putExtra(FileDisplayActivity.Companion.KEY_FILE_PATH, upload.remotePath) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) } activity.startActivity(intent) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListItemOnClick.kt b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListItemOnClick.kt new file mode 100644 index 000000000000..4243adb86006 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListItemOnClick.kt @@ -0,0 +1,14 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.adapter.uploadList.helper + +import com.owncloud.android.db.OCUpload + +interface UploadListItemOnClick { + fun onLastUploadResultConflictClick(upload: OCUpload) +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListSection.kt b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/model/UploadListSection.kt similarity index 97% rename from app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListSection.kt rename to app/src/main/java/com/owncloud/android/ui/adapter/uploadList/model/UploadListSection.kt index 6cb1bcc96995..6513a19d4fd9 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListSection.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/model/UploadListSection.kt @@ -5,7 +5,7 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.owncloud.android.ui.adapter.uploadList +package com.owncloud.android.ui.adapter.uploadList.model import com.owncloud.android.R import com.owncloud.android.datamodel.UploadsStorageManager diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListType.kt b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/model/UploadListType.kt similarity index 80% rename from app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListType.kt rename to app/src/main/java/com/owncloud/android/ui/adapter/uploadList/model/UploadListType.kt index aa2d818f9378..0b6d415fc7fd 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListType.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/model/UploadListType.kt @@ -5,6 +5,6 @@ * SPDX-License-Identifier: AGPL-3.0-or-later */ -package com.owncloud.android.ui.adapter.uploadList +package com.owncloud.android.ui.adapter.uploadList.model enum class UploadListType { CURRENT, COMPLETED, FAILED, CANCELLED, SKIPPED } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 843c2eb35d79..3ecf025b806c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -310,7 +310,6 @@ Unknown error Waiting for non-metered Wi-Fi Waiting to exit power save mode - Fetching server version… Waiting to upload %1$s (%2$d) @@ -1095,7 +1094,10 @@ Locking folder failed Local storage full Encryption is only possible with >= Android 5.0 - Sync conflict, please resolve manually + Checking for upload conflicts… + Upload has no conflicts anymore, conflict cleared. + Could not check for upload conflicts. Please try again. + There might be a conflict from your last upload. Tap to check. Cannot create local file File could not be copied to local storage All uploads are paused @@ -1188,7 +1190,6 @@ Failed to start editor Add folder description We couldnt locate the file on server. Another user may have deleted the file - File not found. Are you sure that this file exists or has a previous conflict not been resolved? File upload conflict Pick which version to keep of %1$s Resolve conflict From 702f2f0bcef10a018d2000da0452aa2bf377083f Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 9 Apr 2026 14:41:19 +0200 Subject: [PATCH 2/6] fix(upload-list): handle conflict actions Signed-off-by: alperozturk96 --- .../client/jobs/BackgroundJobFactory.kt | 5 +- .../client/jobs/upload/FileUploadHelper.kt | 21 ++++--- .../client/jobs/upload/FileUploadWorker.kt | 23 +------- .../factory/UploadFileOperationFactory.kt | 53 +++++++++++++++++ .../android/ui/activity/UploadListActivity.kt | 59 ++++++++++++------- .../adapter/uploadList/UploadListAdapter.kt | 38 ------------ .../helper/UploadListAdapterActionHandler.kt | 6 +- 7 files changed, 115 insertions(+), 90 deletions(-) create mode 100644 app/src/main/java/com/owncloud/android/operations/factory/UploadFileOperationFactory.kt 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 5e573b1c4cd0..b9145d7e8769 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt @@ -36,6 +36,7 @@ import com.nextcloud.client.preferences.AppPreferences import com.owncloud.android.datamodel.ArbitraryDataProvider import com.owncloud.android.datamodel.SyncedFolderProvider import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.operations.factory.UploadFileOperationFactory import com.owncloud.android.utils.theme.ViewThemeUtils import org.greenrobot.eventbus.EventBus import javax.inject.Inject @@ -66,7 +67,8 @@ class BackgroundJobFactory @Inject constructor( private val localBroadcastManager: Provider, private val generatePdfUseCase: GeneratePDFUseCase, private val syncedFolderProvider: SyncedFolderProvider, - private val database: NextcloudDatabase + private val database: NextcloudDatabase, + private val uploadFileOperationFactory: UploadFileOperationFactory ) : WorkerFactory() { @SuppressLint("NewApi") @@ -247,6 +249,7 @@ class BackgroundJobFactory @Inject constructor( FileSystemRepository(dao = database.fileSystemDao(), uploadsStorageManager, context), syncedFolderProvider, context, + uploadFileOperationFactory, params ) diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt index 579b534a21d3..5e011526f95e 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt @@ -37,6 +37,7 @@ import com.owncloud.android.db.OCUpload import com.owncloud.android.db.UploadResult import com.owncloud.android.files.services.NameCollisionPolicy import com.owncloud.android.lib.common.OwnCloudClient +import com.owncloud.android.lib.common.OwnCloudClientFactory import com.owncloud.android.lib.common.network.OnDatatransferProgressListener import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC @@ -46,6 +47,8 @@ import com.owncloud.android.lib.resources.files.model.ServerFileInterface import com.owncloud.android.lib.resources.status.OCCapability import com.owncloud.android.operations.RemoveFileOperation import com.owncloud.android.operations.UploadFileOperation +import com.owncloud.android.ui.adapter.uploadList.helper.ConflictHandlingResult +import com.owncloud.android.ui.adapter.uploadList.helper.UploadListAdapterActionHandler import com.owncloud.android.utils.DisplayUtils import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -167,25 +170,28 @@ class FileUploadHelper { } @Suppress("ComplexCondition") - private fun retryUploads( + private suspend fun retryUploads( uploadsStorageManager: UploadsStorageManager, connectivityService: ConnectivityService, accountManager: UserAccountManager, powerManagementService: PowerManagementService, uploads: List - ): Boolean { + ): Boolean = withContext(Dispatchers.IO) { var showNotExistMessage = false - var showSyncConflictNotification = false + var conflictHandlingResult: ConflictHandlingResult? = null val isOnline = checkConnectivity(connectivityService) val connectivity = connectivityService.connectivity val batteryStatus = powerManagementService.battery val uploadsToRetry = mutableListOf() + val ownCloudClient = + OwnCloudClientFactory.createOwnCloudClient(accountManager.currentAccount, MainApp.getAppContext()) + val uploadActionHandler = UploadListAdapterActionHandler() for (upload in uploads) { if (upload.isLastResultConflictError()) { - Log_OC.d(TAG, "retry upload skipped, sync conflict: ${upload.remotePath}") - showSyncConflictNotification = true + conflictHandlingResult = + uploadActionHandler.handleConflict(upload, ownCloudClient, uploadsStorageManager) continue } @@ -226,11 +232,12 @@ class FileUploadHelper { ) } - if (showSyncConflictNotification) { + if (conflictHandlingResult is ConflictHandlingResult.ShowConflictResolveDialog) { + Log_OC.d(TAG, "retry upload skipped, sync conflict: ${conflictHandlingResult.file.remotePath}") AppWideNotificationManager.showSyncConflictNotification(MainApp.getAppContext()) } - return showNotExistMessage + return@withContext showNotExistMessage } @JvmOverloads diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt index 4b4450f21434..359bacdd05a2 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt @@ -29,7 +29,6 @@ import com.nextcloud.utils.extensions.getPercent import com.nextcloud.utils.extensions.toFile import com.nextcloud.utils.extensions.updateStatus import com.owncloud.android.R -import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.ForegroundServiceType import com.owncloud.android.datamodel.SyncedFolder import com.owncloud.android.datamodel.SyncedFolderProvider @@ -44,6 +43,7 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.UploadFileOperation +import com.owncloud.android.operations.factory.UploadFileOperationFactory import com.owncloud.android.ui.notifications.NotificationUtils import com.owncloud.android.utils.theme.ViewThemeUtils import kotlinx.coroutines.Dispatchers @@ -66,6 +66,7 @@ class FileUploadWorker( val filesystemRepository: FileSystemRepository, val syncedFolderProvider: SyncedFolderProvider, val context: Context, + val uploadFileOperationFactory: UploadFileOperationFactory, params: WorkerParameters ) : CoroutineWorker(context, params), OnDatatransferProgressListener { @@ -270,7 +271,7 @@ class FileUploadWorker( } fileUploadEventBroadcaster.sendUploadEnqueued(context) - val operation = createUploadFileOperation(upload, user) + val operation = uploadFileOperationFactory.create(upload, this@FileUploadWorker) activeOperations[upload.uploadId] = operation val currentIndex = (index + 1) @@ -348,24 +349,6 @@ class FileUploadWorker( return result } - private fun createUploadFileOperation(upload: OCUpload, user: User): UploadFileOperation = UploadFileOperation( - uploadsStorageManager, - connectivityService, - powerManagementService, - user, - null, - upload, - upload.nameCollisionPolicy, - upload.localAction, - context, - upload.isUseWifiOnly, - upload.isWhileChargingOnly, - true, - FileDataStorageManager(user, context.contentResolver) - ).apply { - addDataTransferProgressListener(this@FileUploadWorker) - } - @Suppress("TooGenericExceptionCaught", "DEPRECATION") private suspend fun upload( upload: OCUpload, diff --git a/app/src/main/java/com/owncloud/android/operations/factory/UploadFileOperationFactory.kt b/app/src/main/java/com/owncloud/android/operations/factory/UploadFileOperationFactory.kt new file mode 100644 index 000000000000..9fc0d558710b --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/factory/UploadFileOperationFactory.kt @@ -0,0 +1,53 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.operations.factory + +import android.content.Context +import com.nextcloud.client.account.UserAccountManager +import com.nextcloud.client.device.PowerManagementService +import com.nextcloud.client.network.ConnectivityService +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.db.OCUpload +import com.owncloud.android.files.services.NameCollisionPolicy +import com.owncloud.android.lib.common.network.OnDatatransferProgressListener +import com.owncloud.android.operations.UploadFileOperation +import javax.inject.Inject + +@Suppress("LongParameterList") +class UploadFileOperationFactory @Inject constructor( + private val uploadsStorageManager: UploadsStorageManager, + private val connectivityService: ConnectivityService, + private val powerManagementService: PowerManagementService, + private val context: Context, + private val accountManager: UserAccountManager, + private val fileDataStorageManager: FileDataStorageManager +) { + + fun create( + upload: OCUpload, + progressListener: OnDatatransferProgressListener? = null, + disableRetries: Boolean = true + ): UploadFileOperation = UploadFileOperation( + uploadsStorageManager, + connectivityService, + powerManagementService, + accountManager.user, + null, + upload, + upload.nameCollisionPolicy ?: NameCollisionPolicy.ASK_USER, + upload.localAction, + context, + upload.isUseWifiOnly, + upload.isWhileChargingOnly, + disableRetries, + fileDataStorageManager + ).apply { + progressListener?.let { addDataTransferProgressListener(it) } + } +} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt index 9437e52dac53..92bbc3c59ff1 100755 --- a/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.kt @@ -37,6 +37,7 @@ import com.owncloud.android.lib.common.operations.RemoteOperation import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.CheckCurrentCredentialsOperation +import com.owncloud.android.operations.factory.UploadFileOperationFactory import com.owncloud.android.ui.adapter.uploadList.UploadListAdapter import com.owncloud.android.ui.adapter.uploadList.helper.ConflictHandlingResult import com.owncloud.android.ui.adapter.uploadList.helper.UploadListAdapterAction @@ -44,7 +45,6 @@ import com.owncloud.android.ui.adapter.uploadList.helper.UploadListAdapterAction import com.owncloud.android.ui.adapter.uploadList.helper.UploadListAdapterHelper import com.owncloud.android.ui.adapter.uploadList.helper.UploadListItemOnClick import com.owncloud.android.ui.decoration.MediaGridItemDecoration -import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.FilesSyncHelper import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -67,6 +67,8 @@ class UploadListActivity : @Inject lateinit var throttler: Throttler + @Inject lateinit var uploadFileOperationFactory: UploadFileOperationFactory + private var swipeListRefreshLayout: SwipeRefreshLayout? = null private var binding: UploadListLayoutBinding? = null @@ -272,8 +274,16 @@ class UploadListActivity : } } + private var conflictSnackbar: Snackbar? = null + override fun onLastUploadResultConflictClick(upload: OCUpload) { - DisplayUtils.showSnackMessage(this, R.string.upload_sync_conflict_checking) + val rootView = binding?.root ?: return + + conflictSnackbar = Snackbar.make( + rootView, + R.string.upload_sync_conflict_checking, + Snackbar.LENGTH_INDEFINITE + ).apply { show() } lifecycleScope.launch { val client = clientRepository.getOwncloudClient() ?: return@launch @@ -282,18 +292,15 @@ class UploadListActivity : withContext(Dispatchers.Main) { when (result) { is ConflictHandlingResult.ConflictNotExists -> { - uploadListAdapter.notifyUploadChanged(upload) - onConflictNotExists(upload) + showConflictNotExists(upload) } is ConflictHandlingResult.CannotCheckConflict -> { - DisplayUtils.showSnackMessage( - this@UploadListActivity, - R.string.upload_sync_conflict_check_error - ) + showConflictError() } is ConflictHandlingResult.ShowConflictResolveDialog -> { + conflictSnackbar?.dismiss() adapterHelper.openConflictActivity(result.file, result.upload) } } @@ -301,22 +308,32 @@ class UploadListActivity : } } - private fun onConflictNotExists(upload: OCUpload) { - val rootView = binding?.root ?: return - val snackbar = Snackbar.make( - rootView, - R.string.upload_sync_conflict_not_exists, - Snackbar.LENGTH_LONG - ) - - snackbar.setAction(R.string.retry) { - val optionalUser = userAccountManager.getUser(upload.accountName) - if (optionalUser.isPresent) { - FileUploadHelper.instance().retryUpload(upload, optionalUser.get()) + private fun showConflictNotExists(upload: OCUpload) { + conflictSnackbar?.apply { + setText(R.string.upload_sync_conflict_not_exists) + setDuration(Snackbar.LENGTH_LONG) + setAction(R.string.retry) { + lifecycleScope.launch(Dispatchers.IO) { + val client = clientRepository.getOwncloudClient() + val operation = uploadFileOperationFactory.create(upload).execute(client) + if (operation.isSuccess) { + withContext(Dispatchers.Main) { + uploadListAdapter.loadUploadItemsFromDb() + } + } + } } + show() } + } - snackbar.show() + private fun showConflictError() { + conflictSnackbar?.apply { + setText(R.string.upload_sync_conflict_check_error) + setDuration(Snackbar.LENGTH_LONG) + setAction(null, null) + show() + } } private inner class UploadFinishReceiver : BroadcastReceiver() { diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt index 558ac721a3f3..33b0a835cad1 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt @@ -10,7 +10,6 @@ package com.owncloud.android.ui.adapter.uploadList import android.annotation.SuppressLint import android.app.NotificationManager import android.content.Context -import android.os.Looper import android.text.format.DateUtils import android.view.LayoutInflater import android.view.View @@ -671,43 +670,6 @@ class UploadListAdapter( notificationManager?.cancel(upload.uploadId.toInt()) } - fun notifyUploadChanged(upload: OCUpload) { - for (sectionIndex in uploadListSections.indices) { - val section = uploadListSections[sectionIndex] - - val itemIndex = section.items.indexOfFirst { it.uploadId == upload.uploadId } - - if (itemIndex != -1) { - val adapterPosition = getAdapterPosition(sectionIndex, itemIndex) - - if (adapterPosition != -1) { - val updatedItems = section.items.toMutableList().apply { - this[itemIndex] = upload - } - - uploadListSections[sectionIndex] = section.withItems(updatedItems) - - if (Looper.myLooper() == Looper.getMainLooper()) { - notifyItemChanged(adapterPosition) - } else { - activity.runOnUiThread { - notifyItemChanged(adapterPosition) - } - } - } - return - } - } - } - - private fun getAdapterPosition(section: Int, relativePosition: Int): Int { - var position = 0 - for (i in 0 until section) { - position += uploadListSections[i].items.size + 1 - } - return position + 1 + relativePosition - } - companion object { private val TAG: String = UploadListAdapter::class.java.getSimpleName() } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListAdapterActionHandler.kt b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListAdapterActionHandler.kt index d8590b6db9fe..b92d29b005d2 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListAdapterActionHandler.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/helper/UploadListAdapterActionHandler.kt @@ -8,9 +8,9 @@ package com.owncloud.android.ui.adapter.uploadList.helper import com.nextcloud.utils.extensions.isSame +import com.nextcloud.utils.extensions.updateStatus import com.owncloud.android.datamodel.UploadsStorageManager import com.owncloud.android.db.OCUpload -import com.owncloud.android.db.UploadResult import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC @@ -64,8 +64,8 @@ class UploadListAdapterActionHandler : UploadListAdapterAction { upload: OCUpload, storageManager: UploadsStorageManager ): ConflictHandlingResult.ConflictNotExists { - upload.lastResult = UploadResult.UNKNOWN - storageManager.updateUpload(upload) + val entity = storageManager.uploadDao.getUploadById(upload.uploadId, upload.accountName) + storageManager.updateStatus(entity, UploadsStorageManager.UploadStatus.UPLOAD_FAILED) return ConflictHandlingResult.ConflictNotExists } } From c68ac1fdf84bea4303a9b765b77453faa1841da0 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 9 Apr 2026 14:48:32 +0200 Subject: [PATCH 3/6] fix(upload-list): handle conflict actions Signed-off-by: alperozturk96 --- .../client/jobs/upload/FileUploadHelper.kt | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt index 5e011526f95e..b4dea7a2b062 100644 --- a/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt +++ b/app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt @@ -25,6 +25,7 @@ import com.nextcloud.client.network.ConnectivityService import com.nextcloud.client.notifications.AppWideNotificationManager import com.nextcloud.utils.extensions.checkWCFRestrictions import com.nextcloud.utils.extensions.getUploadIds +import com.nextcloud.utils.extensions.isAnonymous import com.nextcloud.utils.extensions.isLastResultConflictError import com.nextcloud.utils.extensions.isSame import com.owncloud.android.MainApp @@ -184,14 +185,22 @@ class FileUploadHelper { val batteryStatus = powerManagementService.battery val uploadsToRetry = mutableListOf() - val ownCloudClient = - OwnCloudClientFactory.createOwnCloudClient(accountManager.currentAccount, MainApp.getAppContext()) + + val currentAccount = accountManager.currentAccount + val context = MainApp.getAppContext() + var ownCloudClient: OwnCloudClient? = null + if (!currentAccount.isAnonymous(context)) { + ownCloudClient = + OwnCloudClientFactory.createOwnCloudClient(accountManager.currentAccount, MainApp.getAppContext()) + } val uploadActionHandler = UploadListAdapterActionHandler() for (upload in uploads) { if (upload.isLastResultConflictError()) { - conflictHandlingResult = - uploadActionHandler.handleConflict(upload, ownCloudClient, uploadsStorageManager) + ownCloudClient?.let { + conflictHandlingResult = + uploadActionHandler.handleConflict(upload, ownCloudClient, uploadsStorageManager) + } continue } From 61cedb32589b6cde24e025d60b4f585dcb0637cc Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 9 Apr 2026 15:05:33 +0200 Subject: [PATCH 4/6] fix(upload-list): handle conflict actions Signed-off-by: alperozturk96 --- .../java/com/nextcloud/utils/OCFileUtils.kt | 4 ++-- .../utils/extensions/RemoteFileExtensions.kt | 24 +++++++++++++++++-- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/nextcloud/utils/OCFileUtils.kt b/app/src/main/java/com/nextcloud/utils/OCFileUtils.kt index cdcc3506e018..02a97548e9f6 100644 --- a/app/src/main/java/com/nextcloud/utils/OCFileUtils.kt +++ b/app/src/main/java/com/nextcloud/utils/OCFileUtils.kt @@ -55,7 +55,7 @@ object OCFileUtils { return fallbackPair } - private fun getExifSize(path: String): Pair? = try { + fun getExifSize(path: String): Pair? = try { val exif = ExifInterface(path) var w = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0) var h = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0) @@ -78,7 +78,7 @@ object OCFileUtils { null } - private fun getBitmapSize(path: String): Pair? = try { + fun getBitmapSize(path: String): Pair? = try { val options = android.graphics.BitmapFactory.Options().apply { inJustDecodeBounds = true } android.graphics.BitmapFactory.decodeFile(path, options) val w = options.outWidth diff --git a/app/src/main/java/com/nextcloud/utils/extensions/RemoteFileExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/RemoteFileExtensions.kt index 976ce99e51bc..5e6628d0bb32 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/RemoteFileExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/RemoteFileExtensions.kt @@ -7,14 +7,16 @@ package com.nextcloud.utils.extensions +import com.nextcloud.utils.OCFileUtils import com.nextcloud.utils.TimeConstants import com.owncloud.android.lib.resources.files.model.RemoteFile import com.owncloud.android.utils.FileUtil +import com.owncloud.android.utils.MimeTypeUtil fun RemoteFile.isSame(path: String?): Boolean { val localFile = path?.toFile() ?: return false - // remote file timestamp in milli not micro sec + // remote file timestamp in millisecond not microsecond val localLastModifiedTimestamp = localFile.lastModified() / TimeConstants.MILLIS_PER_SECOND val localCreationTimestamp = FileUtil.getCreationTimestamp(localFile) val localSize: Long = localFile.length() @@ -22,5 +24,23 @@ fun RemoteFile.isSame(path: String?): Boolean { return size == localSize && localCreationTimestamp != null && localCreationTimestamp == creationTimestamp && - modifiedTimestamp == localLastModifiedTimestamp * TimeConstants.MILLIS_PER_SECOND + modifiedTimestamp == localLastModifiedTimestamp * TimeConstants.MILLIS_PER_SECOND && + this.areImageDimensionsSame(path) +} + +@Suppress("ReturnCount") +private fun RemoteFile.areImageDimensionsSame(path: String): Boolean { + if (!MimeTypeUtil.isImage(mimeType)) { + // can't compare it's not image + return true + } + + val localFileImageDimension = OCFileUtils.getExifSize(path) ?: OCFileUtils.getBitmapSize(path) + if (localFileImageDimension == null) { + // can't compare local file image dimension is not determined + return true + } + + return localFileImageDimension.first.toFloat() == imageDimension?.width && + localFileImageDimension.second.toFloat() == imageDimension?.height } From 44c8726dee20149bd5b357096a29938ba7c49dbd Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 9 Apr 2026 15:15:15 +0200 Subject: [PATCH 5/6] wip Signed-off-by: alperozturk96 --- .../java/com/nextcloud/utils/OCFileUtils.kt | 42 ++----------------- .../utils/extensions/FileExtensions.kt | 36 ++++++++++++++++ .../utils/extensions/RemoteFileExtensions.kt | 3 +- 3 files changed, 41 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/com/nextcloud/utils/OCFileUtils.kt b/app/src/main/java/com/nextcloud/utils/OCFileUtils.kt index 02a97548e9f6..d16ea8eef159 100644 --- a/app/src/main/java/com/nextcloud/utils/OCFileUtils.kt +++ b/app/src/main/java/com/nextcloud/utils/OCFileUtils.kt @@ -11,7 +11,8 @@ import android.graphics.drawable.BitmapDrawable import androidx.core.content.ContextCompat import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toDrawable -import androidx.exifinterface.media.ExifInterface +import com.nextcloud.utils.extensions.getBitmapSize +import com.nextcloud.utils.extensions.getExifSize import com.owncloud.android.MainApp import com.owncloud.android.R import com.owncloud.android.datamodel.OCFile @@ -41,8 +42,8 @@ object OCFileUtils { // Local file val path = ocFile.storagePath if (!path.isNullOrEmpty() && ocFile.exists()) { - getExifSize(path)?.let { return it } - getBitmapSize(path)?.let { return it } + path.getExifSize()?.let { return it } + path.getBitmapSize()?.let { return it } } // 3 Fallback @@ -55,41 +56,6 @@ object OCFileUtils { return fallbackPair } - fun getExifSize(path: String): Pair? = try { - val exif = ExifInterface(path) - var w = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0) - var h = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0) - - val orientation = exif.getAttributeInt( - ExifInterface.TAG_ORIENTATION, - ExifInterface.ORIENTATION_NORMAL - ) - if (orientation == ExifInterface.ORIENTATION_ROTATE_90 || - orientation == ExifInterface.ORIENTATION_ROTATE_270 - ) { - val tmp = w - w = h - h = tmp - } - - Log_OC.d(TAG, "Using exif imageDimension: $w x $h") - if (w > 0 && h > 0) w to h else null - } catch (_: Exception) { - null - } - - fun getBitmapSize(path: String): Pair? = try { - val options = android.graphics.BitmapFactory.Options().apply { inJustDecodeBounds = true } - android.graphics.BitmapFactory.decodeFile(path, options) - val w = options.outWidth - val h = options.outHeight - - Log_OC.d(TAG, "Using bitmap factory imageDimension: $w x $h") - if (w > 0 && h > 0) w to h else null - } catch (_: Exception) { - null - } - fun getMediaPlaceholder(file: OCFile, imageDimension: Pair): BitmapDrawable { val context = MainApp.getAppContext() diff --git a/app/src/main/java/com/nextcloud/utils/extensions/FileExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/FileExtensions.kt index 5bae77a44d61..1804550bff03 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/FileExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/FileExtensions.kt @@ -7,6 +7,7 @@ package com.nextcloud.utils.extensions +import androidx.exifinterface.media.ExifInterface import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.utils.DisplayUtils @@ -50,3 +51,38 @@ fun String.toFile(): File? { return file } + +fun String.getExifSize(): Pair? = try { + val exif = ExifInterface(this) + var w = exif.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, 0) + var h = exif.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, 0) + + val orientation = exif.getAttributeInt( + ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_NORMAL + ) + if (orientation == ExifInterface.ORIENTATION_ROTATE_90 || + orientation == ExifInterface.ORIENTATION_ROTATE_270 + ) { + val tmp = w + w = h + h = tmp + } + + Log_OC.d(TAG, "Using exif imageDimension: $w x $h") + if (w > 0 && h > 0) w to h else null +} catch (_: Exception) { + null +} + +fun String.getBitmapSize(): Pair? = try { + val options = android.graphics.BitmapFactory.Options().apply { inJustDecodeBounds = true } + android.graphics.BitmapFactory.decodeFile(this, options) + val w = options.outWidth + val h = options.outHeight + + Log_OC.d(TAG, "Using bitmap factory imageDimension: $w x $h") + if (w > 0 && h > 0) w to h else null +} catch (_: Exception) { + null +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/RemoteFileExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/RemoteFileExtensions.kt index 5e6628d0bb32..114849b5a831 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/RemoteFileExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/RemoteFileExtensions.kt @@ -7,7 +7,6 @@ package com.nextcloud.utils.extensions -import com.nextcloud.utils.OCFileUtils import com.nextcloud.utils.TimeConstants import com.owncloud.android.lib.resources.files.model.RemoteFile import com.owncloud.android.utils.FileUtil @@ -35,7 +34,7 @@ private fun RemoteFile.areImageDimensionsSame(path: String): Boolean { return true } - val localFileImageDimension = OCFileUtils.getExifSize(path) ?: OCFileUtils.getBitmapSize(path) + val localFileImageDimension = path.getExifSize() ?: path.getBitmapSize() if (localFileImageDimension == null) { // can't compare local file image dimension is not determined return true From 8a3b895b9d6924fa0219d35cdebf2f671479cc30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alper=20=C3=96zt=C3=BCrk?= <67455295+alperozturk96@users.noreply.github.com> Date: Fri, 10 Apr 2026 13:15:51 +0200 Subject: [PATCH 6/6] Update app/src/main/res/values/strings.xml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Laura Kramolis Signed-off-by: Alper Öztürk <67455295+alperozturk96@users.noreply.github.com> --- app/src/main/res/values/strings.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3ecf025b806c..3cbe9a1195e7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1095,7 +1095,7 @@ Local storage full Encryption is only possible with >= Android 5.0 Checking for upload conflicts… - Upload has no conflicts anymore, conflict cleared. + Conflict resolved. Could not check for upload conflicts. Please try again. There might be a conflict from your last upload. Tap to check. Cannot create local file