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 634c60827d1c..a22b5a5999cb 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 @@ -124,8 +124,9 @@ class FileUploadHelper { val capability = fileStorageManager.getCapability(accountManager.user) try { - getUploadsByStatus(null, UploadStatus.UPLOAD_FAILED, capability) { - if (it.isNotEmpty()) { + ioScope.launch { + val uploads = getUploadsByStatus(null, UploadStatus.UPLOAD_FAILED, capability) + if (uploads.isNotEmpty()) { isUploadStarted = true } @@ -134,7 +135,7 @@ class FileUploadHelper { connectivityService, accountManager, powerManagementService, - uploads = it + uploads ) } } finally { @@ -144,26 +145,21 @@ class FileUploadHelper { return isUploadStarted } - fun retryCancelledUploads( + suspend fun retryCancelledUploads( uploadsStorageManager: UploadsStorageManager, connectivityService: ConnectivityService, accountManager: UserAccountManager, powerManagementService: PowerManagementService ): Boolean { - var result = false val capability = fileStorageManager.getCapability(accountManager.user) - - getUploadsByStatus(accountManager.user.accountName, UploadStatus.UPLOAD_CANCELLED, capability) { - result = retryUploads( - uploadsStorageManager, - connectivityService, - accountManager, - powerManagementService, - it - ) - } - - return result + val uploads = getUploadsByStatus(accountManager.user.accountName, UploadStatus.UPLOAD_CANCELLED, capability) + return retryUploads( + uploadsStorageManager, + connectivityService, + accountManager, + powerManagementService, + uploads + ) } @Suppress("ComplexCondition") @@ -172,7 +168,7 @@ class FileUploadHelper { connectivityService: ConnectivityService, accountManager: UserAccountManager, powerManagementService: PowerManagementService, - uploads: Array + uploads: List ): Boolean { var showNotExistMessage = false var showSyncConflictNotification = false @@ -343,31 +339,24 @@ class FileUploadHelper { * belonging to that account are retrieved. If [accountName] is `null`, uploads with the * given [status] from **all user accounts** are returned. * - * Once the uploads are fetched, the [onCompleted] callback is invoked with the resulting array. - * * @param accountName The name of the account to filter uploads by. * If `null`, uploads matching the given [status] from all accounts are returned. * @param status The [UploadStatus] to filter uploads by (e.g., `UPLOAD_FAILED`). * @param nameCollisionPolicy The [NameCollisionPolicy] to filter uploads by (e.g., `SKIP`). - * @param onCompleted A callback invoked with the resulting array of [OCUpload] objects. */ - fun getUploadsByStatus( + suspend fun getUploadsByStatus( accountName: String?, status: UploadStatus, capability: OCCapability, - nameCollisionPolicy: NameCollisionPolicy? = null, - onCompleted: (Array) -> Unit - ) { - ioScope.launch { - val dao = uploadsStorageManager.uploadDao - val result = if (accountName != null) { - dao.getUploadsByAccountNameAndStatus(accountName, status.value, nameCollisionPolicy?.serialize()) - } else { - dao.getUploadsByStatus(status.value, nameCollisionPolicy?.serialize()) - }.mapNotNull { - it.toOCUpload(capability) - }.toTypedArray() - onCompleted(result) + nameCollisionPolicy: NameCollisionPolicy? = null + ): List { + val dao = uploadsStorageManager.uploadDao + return if (accountName != null) { + dao.getUploadsByAccountNameAndStatus(accountName, status.value, nameCollisionPolicy?.serialize()) + } else { + dao.getUploadsByStatus(status.value, nameCollisionPolicy?.serialize()) + }.mapNotNull { + it.toOCUpload(capability) } } diff --git a/app/src/main/java/com/nextcloud/utils/extensions/OCUploadExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/OCUploadExtensions.kt index 6b2550df1e60..df2b1c68059d 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/OCUploadExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/OCUploadExtensions.kt @@ -7,8 +7,61 @@ package com.nextcloud.utils.extensions +import android.content.Context +import com.nextcloud.client.preferences.AppPreferences +import com.owncloud.android.R +import com.owncloud.android.datamodel.UploadsStorageManager import com.owncloud.android.db.OCUpload +import com.owncloud.android.db.UploadResult +import com.owncloud.android.files.services.NameCollisionPolicy fun List.getUploadIds(): LongArray = map { it.uploadId }.toLongArray() fun Array.getUploadIds(): LongArray = map { it.uploadId }.toLongArray() + +fun List.sortedByUploadOrder(): List = sortedWith( + compareBy { it.fixedUploadStatus } + .thenByDescending { it.isFixedUploadingNow } + .thenByDescending { it.fixedUploadEndTimeStamp } + .thenBy { it.fixedUploadId } +) + +fun OCUpload.getStatusText(activity: Context, isGlobalUploadPaused: Boolean, isUploading: Boolean): String { + val status: String + val res = activity.resources + when (val uploadStatus = uploadStatus) { + UploadsStorageManager.UploadStatus.UPLOAD_IN_PROGRESS -> { + status = if (isGlobalUploadPaused) { + res.getString(R.string.upload_global_pause_title) + } else if (isUploading) { + res.getString(R.string.uploader_upload_in_progress_ticker) + } else { + res.getString(R.string.uploads_view_later_waiting_to_upload) + } + } + + UploadsStorageManager.UploadStatus.UPLOAD_SUCCEEDED -> { + status = if (lastResult == UploadResult.SAME_FILE_CONFLICT) { + res.getString(R.string.uploads_view_upload_status_succeeded_same_file) + } else if (lastResult == UploadResult.FILE_NOT_FOUND) { + lastResult.getFailedStatusText(activity) + } else if (nameCollisionPolicy == NameCollisionPolicy.SKIP) { + res.getString(R.string.uploads_view_upload_status_skip_reason) + } else { + res.getString(R.string.uploads_view_upload_status_succeeded) + } + } + + UploadsStorageManager.UploadStatus.UPLOAD_FAILED -> + status = + lastResult.getFailedStatusText(activity) + + UploadsStorageManager.UploadStatus.UPLOAD_CANCELLED -> + status = + res.getString(R.string.upload_manually_cancelled) + + else -> status = "Uncontrolled status: $uploadStatus" + } + + return status +} 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 9c1f1218e57a..0a9da25da2e9 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/UploadResultExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/UploadResultExtensions.kt @@ -7,6 +7,8 @@ package com.nextcloud.utils.extensions +import android.content.Context +import com.owncloud.android.R import com.owncloud.android.db.UploadResult fun UploadResult.isNonRetryable(): Boolean = when (this) { @@ -32,3 +34,72 @@ fun UploadResult.isNonRetryable(): Boolean = when (this) { // everything else may succeed after retry else -> false } + +fun UploadResult.getFailedStatusText(context: Context): String = when (this) { + UploadResult.CREDENTIAL_ERROR -> + context.getString(R.string.uploads_view_upload_status_failed_credentials_error) + + UploadResult.FOLDER_ERROR -> + context.getString(R.string.uploads_view_upload_status_failed_folder_error) + + UploadResult.FILE_NOT_FOUND -> + context.getString(R.string.uploads_view_upload_status_failed_localfile_error) + + UploadResult.FILE_ERROR -> context.getString(R.string.uploads_view_upload_status_failed_file_error) + + UploadResult.PRIVILEGES_ERROR -> context.getString( + R.string.uploads_view_upload_status_failed_permission_error + ) + + UploadResult.NETWORK_CONNECTION -> + context.getString(R.string.uploads_view_upload_status_failed_connection_error) + + UploadResult.DELAYED_FOR_WIFI -> context.getString( + R.string.uploads_view_upload_status_waiting_for_wifi + ) + + UploadResult.DELAYED_FOR_CHARGING -> + context.getString(R.string.uploads_view_upload_status_waiting_for_charging) + + UploadResult.CONFLICT_ERROR -> context.getString(R.string.uploads_view_upload_status_conflict) + + UploadResult.SERVICE_INTERRUPTED -> context.getString( + R.string.uploads_view_upload_status_service_interrupted + ) + + UploadResult.CANCELLED -> // should not get here ; cancelled uploads should be wiped out + context.getString(R.string.uploads_view_upload_status_cancelled) + + UploadResult.UPLOADED -> // should not get here ; status should be UPLOAD_SUCCESS + context.getString(R.string.uploads_view_upload_status_succeeded) + + UploadResult.MAINTENANCE_MODE -> context.getString(R.string.maintenance_mode) + + UploadResult.SSL_RECOVERABLE_PEER_UNVERIFIED -> context.getString( + R.string.uploads_view_upload_status_failed_ssl_certificate_not_trusted + ) + + UploadResult.UNKNOWN -> context.getString(R.string.uploads_view_upload_status_unknown_fail) + + UploadResult.LOCK_FAILED -> context.getString(R.string.upload_lock_failed) + + UploadResult.DELAYED_IN_POWER_SAVE_MODE -> context.getString( + R.string.uploads_view_upload_status_waiting_exit_power_save_mode + ) + + UploadResult.VIRUS_DETECTED -> context.getString(R.string.uploads_view_upload_status_virus_detected) + + UploadResult.LOCAL_STORAGE_FULL -> context.getString(R.string.upload_local_storage_full) + + UploadResult.OLD_ANDROID_API -> context.getString(R.string.upload_old_android) + + UploadResult.SYNC_CONFLICT -> context.getString(R.string.upload_sync_conflict) + + UploadResult.CANNOT_CREATE_FILE -> context.getString(R.string.upload_cannot_create_file) + + UploadResult.LOCAL_STORAGE_NOT_COPIED -> context.getString(R.string.upload_local_storage_not_copied) + + UploadResult.QUOTA_EXCEEDED -> context.getString(R.string.upload_quota_exceeded) + + else -> context.getString(R.string.upload_unknown_error) +} diff --git a/app/src/main/java/com/owncloud/android/db/OCUploadComparator.kt b/app/src/main/java/com/owncloud/android/db/OCUploadComparator.kt deleted file mode 100644 index 5c73c9f93c58..000000000000 --- a/app/src/main/java/com/owncloud/android/db/OCUploadComparator.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2019 Daniele Fognini - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package com.owncloud.android.db - -/** - * Sorts OCUpload by (uploadStatus, uploadingNow, uploadEndTimeStamp, uploadId). - */ -class OCUploadComparator : Comparator { - @Suppress("ReturnCount") - override fun compare(upload1: OCUpload?, upload2: OCUpload?): Int { - if (upload1 == null && upload2 == null) { - return 0 - } - if (upload1 == null) { - return -1 - } - if (upload2 == null) { - return 1 - } - - val compareUploadStatus = compareUploadStatus(upload1, upload2) - if (compareUploadStatus != 0) { - return compareUploadStatus - } - - val compareUploadingNow = compareUploadingNow(upload1, upload2) - if (compareUploadingNow != 0) { - return compareUploadingNow - } - - val compareUpdateTime = compareUpdateTime(upload1, upload2) - if (compareUpdateTime != 0) { - return compareUpdateTime - } - - val compareUploadId = compareUploadId(upload1, upload2) - if (compareUploadId != 0) { - return compareUploadId - } - - return 0 - } - - private fun compareUploadStatus(upload1: OCUpload, upload2: OCUpload): Int = - upload1.fixedUploadStatus.compareTo(upload2.fixedUploadStatus) - - private fun compareUploadingNow(upload1: OCUpload, upload2: OCUpload): Int = - upload2.isFixedUploadingNow.compareTo(upload1.isFixedUploadingNow) - - private fun compareUpdateTime(upload1: OCUpload, upload2: OCUpload): Int = - upload2.fixedUploadEndTimeStamp.compareTo(upload1.fixedUploadEndTimeStamp) - - private fun compareUploadId(upload1: OCUpload, upload2: OCUpload): Int = - upload1.fixedUploadId.compareTo(upload2.fixedUploadId) -} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java index 280acaf45bad..ec834e983c1d 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java @@ -175,7 +175,7 @@ public abstract class FileActivity extends DrawerActivity protected boolean isFileDisplayActivityResumed = false; @Inject - UserAccountManager accountManager; + public UserAccountManager accountManager; @Inject public ConnectivityService connectivityService; diff --git a/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java index 69893cabee31..d51321efb152 100755 --- a/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java @@ -37,7 +37,7 @@ 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.UploadListAdapter; +import com.owncloud.android.ui.adapter.uploadList.UploadListAdapter; import com.owncloud.android.ui.decoration.MediaGridItemDecoration; import com.owncloud.android.utils.FilesSyncHelper; @@ -317,7 +317,7 @@ public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationRe private class UploadFinishReceiver extends BroadcastReceiver { @Override public void 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/UploadListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java deleted file mode 100755 index d0f239382757..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java +++ /dev/null @@ -1,962 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2019 Chris Narkiewicz - * SPDX-FileCopyrightText: 2018 Tobias Kaminsky - * SPDX-FileCopyrightText: 2018 Nextcloud - * SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only - */ -package com.owncloud.android.ui.adapter; - -import android.annotation.SuppressLint; -import android.app.NotificationManager; -import android.content.ActivityNotFoundException; -import android.content.Context; -import android.content.Intent; -import android.graphics.Bitmap; -import android.graphics.drawable.Drawable; -import android.net.Uri; -import android.text.format.DateUtils; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.PopupMenu; - -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; -import com.nextcloud.client.network.ConnectivityService; -import com.nextcloud.utils.extensions.ViewExtensionsKt; -import com.owncloud.android.MainApp; -import com.owncloud.android.R; -import com.owncloud.android.databinding.UploadListHeaderBinding; -import com.owncloud.android.databinding.UploadListItemBinding; -import com.owncloud.android.datamodel.FileDataStorageManager; -import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.datamodel.ThumbnailsCacheManager; -import com.owncloud.android.datamodel.UploadsStorageManager; -import com.owncloud.android.datamodel.UploadsStorageManager.UploadStatus; -import com.owncloud.android.db.OCUpload; -import com.owncloud.android.db.OCUploadComparator; -import com.owncloud.android.db.UploadResult; -import com.owncloud.android.files.services.NameCollisionPolicy; -import com.owncloud.android.lib.common.operations.OnRemoteOperationListener; -import com.owncloud.android.lib.common.utils.Log_OC; -import com.owncloud.android.operations.RefreshFolderOperation; -import com.owncloud.android.ui.activity.ConflictsResolveActivity; -import com.owncloud.android.ui.activity.FileActivity; -import com.owncloud.android.ui.activity.FileDisplayActivity; -import com.owncloud.android.ui.adapter.progressListener.UploadProgressListener; -import com.owncloud.android.ui.preview.PreviewImageFragment; -import com.owncloud.android.utils.DisplayUtils; -import com.owncloud.android.utils.MimeTypeUtil; -import com.owncloud.android.utils.theme.ViewThemeUtils; - -import java.io.File; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; - -import androidx.annotation.NonNull; -import androidx.core.content.ContextCompat; -import kotlin.Unit; - -/** - * This Adapter populates a ListView with following types of uploads: pending, active, completed. Filtering possible. - */ -public class UploadListAdapter extends SectionedRecyclerViewAdapter { - private static final String TAG = UploadListAdapter.class.getSimpleName(); - - private record Section( - Type type, - int titleRes, - UploadStatus status, - NameCollisionPolicy collisionPolicy, - OCUpload[] items - ) { - Section withItems(OCUpload[] newItems) { - return new Section(type, titleRes, status, collisionPolicy, newItems); - } - } - - private final List
sections = new ArrayList<>(List.of( - new Section(Type.CURRENT, R.string.uploads_view_group_current_uploads, UploadStatus.UPLOAD_IN_PROGRESS, null, new OCUpload[0]), - new Section(Type.FAILED, R.string.uploads_view_group_failed_uploads, UploadStatus.UPLOAD_FAILED, null, new OCUpload[0]), - new Section(Type.CANCELLED, R.string.uploads_view_group_manually_cancelled_uploads, UploadStatus.UPLOAD_CANCELLED, null, new OCUpload[0]), - new Section(Type.COMPLETED, R.string.uploads_view_group_completed_uploads, UploadStatus.UPLOAD_SUCCEEDED, NameCollisionPolicy.ASK_USER, new OCUpload[0]), - new Section(Type.SKIPPED, R.string.uploads_view_upload_status_skip, UploadStatus.UPLOAD_SUCCEEDED, NameCollisionPolicy.SKIP, new OCUpload[0]))); - - private UploadProgressListener uploadProgressListener; - private final FileActivity parentActivity; - private final UploadsStorageManager uploadsStorageManager; - private final FileDataStorageManager storageManager; - private final ConnectivityService connectivityService; - private final PowerManagementService powerManagementService; - private final UserAccountManager accountManager; - private final Clock clock; - private final boolean showUser; - private final ViewThemeUtils viewThemeUtils; - private NotificationManager mNotificationManager; - private final FileUploadHelper uploadHelper = FileUploadHelper.Companion.instance(); - - public UploadListAdapter(final FileActivity fileActivity, - final UploadsStorageManager uploadsStorageManager, - final FileDataStorageManager storageManager, - final UserAccountManager accountManager, - final ConnectivityService connectivityService, - final PowerManagementService powerManagementService, - final Clock clock, - final ViewThemeUtils viewThemeUtils) { - Log_OC.d(TAG, "UploadListAdapter"); - - this.parentActivity = fileActivity; - this.uploadsStorageManager = uploadsStorageManager; - this.storageManager = storageManager; - this.accountManager = accountManager; - this.connectivityService = connectivityService; - this.powerManagementService = powerManagementService; - this.clock = clock; - this.viewThemeUtils = viewThemeUtils; - shouldShowHeadersForEmptySections(false); - showUser = accountManager.getAccounts().length > 1; - } - - @Override - public int getSectionCount() { - return sections.size(); - } - - @Override - public int getItemCount(int section) { - return sections.get(section).items().length; - } - - @Override - public void onBindHeaderViewHolder(SectionedViewHolder holder, int section, boolean expanded) { - HeaderViewHolder headerViewHolder = (HeaderViewHolder) holder; - - Section group = sections.get(section); - String title = parentActivity.getString(group.titleRes()); - int count = group.items().length; - - headerViewHolder.binding.uploadListTitle.setText( - String.format(parentActivity.getString(R.string.uploads_view_group_header), title, count)); - viewThemeUtils.platform.colorPrimaryTextViewElement(headerViewHolder.binding.uploadListTitle); - - headerViewHolder.binding.uploadListTitle.setOnClickListener(v -> { - toggleSectionExpanded(section); - headerViewHolder.binding.uploadListState.setImageResource(isSectionExpanded(section) ? - R.drawable.ic_expand_less : - R.drawable.ic_expand_more); - }); - - headerViewHolder.binding.uploadListStateLayout.setOnClickListener(v -> {{ - toggleSectionExpanded(section); - headerViewHolder.binding.uploadListState.setImageResource(isSectionExpanded(section) ? - R.drawable.ic_expand_less : - R.drawable.ic_expand_more); - }}); - - switch (group.type) { - case CURRENT, COMPLETED -> headerViewHolder.binding.uploadListAction.setImageResource(R.drawable.ic_close); - case CANCELLED, FAILED -> - headerViewHolder.binding.uploadListAction.setImageResource(R.drawable.ic_dots_vertical); - - } - - ViewExtensionsKt.setVisibleIf(headerViewHolder.binding.autoUploadBatterySaverWarningCard.root, - powerManagementService.isPowerSavingEnabled()); - viewThemeUtils.material.themeCardView(headerViewHolder.binding.autoUploadBatterySaverWarningCard.root); - - headerViewHolder.binding.uploadListAction.setOnClickListener(v -> { - switch (group.type) { - case CURRENT -> { - OCUpload[] items = group.items(); - if (items.length == 0) return; - - String accountName = items[0].getAccountName(); - - final int totalUploads = group.items().length; - final int[] completedCount = {0}; - - for (int i=0; i { - FileUploadWorker.Companion.cancelUpload(upload.getRemotePath(), accountName, () -> { - completedCount[0]++; - if (completedCount[0] == totalUploads) { - Log_OC.d(TAG, "refreshing upload items"); - - // All uploads finished, refresh UI once - loadUploadItemsFromDb(() -> {}); - } - return Unit.INSTANCE; - }); - return Unit.INSTANCE; - }); - } - } - case COMPLETED -> { - uploadsStorageManager.clearSuccessfulUploads(); - loadUploadItemsFromDb(() -> {}); - } - case FAILED -> showFailedPopupMenu(headerViewHolder); - case CANCELLED -> showCancelledPopupMenu(headerViewHolder); - default -> { - - } - } - }); - } - - private void showFailedPopupMenu(HeaderViewHolder headerViewHolder) { - PopupMenu failedPopup = new PopupMenu(MainApp.getAppContext(), headerViewHolder.binding.uploadListAction); - failedPopup.inflate(R.menu.upload_list_failed_options); - failedPopup.setOnMenuItemClickListener(i -> { - int itemId = i.getItemId(); - - if (itemId == R.id.action_upload_list_failed_clear) { - uploadsStorageManager.clearFailedButNotDelayedUploads(); - clearTempEncryptedFolder(); - loadUploadItemsFromDb(() -> {}); - } else if (itemId == R.id.action_upload_list_failed_retry) { - uploadHelper.retryFailedUploads( - uploadsStorageManager, - connectivityService, - accountManager, - powerManagementService); - } - - return true; - }); - - failedPopup.show(); - } - - private void showCancelledPopupMenu(HeaderViewHolder headerViewHolder) { - PopupMenu popup = new PopupMenu(MainApp.getAppContext(), headerViewHolder.binding.uploadListAction); - popup.inflate(R.menu.upload_list_cancelled_options); - - popup.setOnMenuItemClickListener(i -> { - int itemId = i.getItemId(); - - if (itemId == R.id.action_upload_list_cancelled_clear) { - uploadsStorageManager.clearCancelledUploadsForCurrentAccount(); - loadUploadItemsFromDb(() -> {}); - clearTempEncryptedFolder(); - } else if (itemId == R.id.action_upload_list_cancelled_resume) { - retryCancelledUploads(); - } - - return true; - }); - - popup.show(); - } - - private void clearTempEncryptedFolder() { - Optional user = parentActivity.getUser(); - user.ifPresent(value -> FileDataStorageManager.clearTempEncryptedFolder(value.getAccountName())); - } - - // FIXME For e2e resume is not working - private void retryCancelledUploads() { - new Thread(() -> { - boolean showNotExistMessage = uploadHelper.retryCancelledUploads( - uploadsStorageManager, - connectivityService, - accountManager, - powerManagementService); - parentActivity.runOnUiThread(() -> { - if (showNotExistMessage) { - showNotExistMessage(); - } - }); - }).start(); - } - - private void showNotExistMessage() { - DisplayUtils.showSnackMessage(parentActivity, R.string.upload_action_file_not_exist_message); - } - - @Override - public void onBindFooterViewHolder(SectionedViewHolder holder, int section) { - // not needed - } - - @Override - public void onBindViewHolder(SectionedViewHolder holder, int section, int relativePosition, int absolutePosition) { - if (sections.isEmpty() || section < 0 || section >= sections.size()) { - return; - } - - Section sectionData = sections.get(section); - OCUpload item = sectionData.items()[relativePosition]; - - if (item == null) { - return; - } - - ItemViewHolder itemViewHolder = (ItemViewHolder) holder; - itemViewHolder.binding.uploadName.setText(item.getLocalPath()); - - // local file name - File remoteFile = new File(item.getRemotePath()); - String fileName = remoteFile.getName(); - if (fileName.isEmpty()) { - fileName = File.separator; - } - itemViewHolder.binding.uploadName.setText(fileName); - - // remote path to parent folder - itemViewHolder.binding.uploadRemotePath.setText(new File(item.getRemotePath()).getParent()); - - long updateTime = item.getUploadEndTimestamp(); - - // file size - if (item.getFileSize() != 0) { - String fileSizeFormat = "%s "; - - // we have valid update time so we can show the upload date - if (updateTime > 0) { - fileSizeFormat = "%s, "; - } - - String fileSizeInBytes = DisplayUtils.bytesToHumanReadable(item.getFileSize()); - String uploadFileSize = String.format(fileSizeFormat, fileSizeInBytes); - itemViewHolder.binding.uploadFileSize.setText(uploadFileSize); - } else { - itemViewHolder.binding.uploadFileSize.setText(""); - } - - // upload date - boolean showUploadDate = (updateTime > 0 && item.getUploadStatus() == UploadStatus.UPLOAD_SUCCEEDED && - item.getLastResult() == UploadResult.UPLOADED); - itemViewHolder.binding.uploadDate.setVisibility(showUploadDate ? View.VISIBLE : View.GONE); - if (showUploadDate) { - CharSequence dateString = DisplayUtils.getRelativeDateTimeString(parentActivity, - updateTime, - DateUtils.MINUTE_IN_MILLIS, - DateUtils.WEEK_IN_MILLIS, - 0); - itemViewHolder.binding.uploadDate.setText(dateString); - } - - // account - final Optional optionalUser = accountManager.getUser(item.getAccountName()); - if (showUser) { - itemViewHolder.binding.uploadAccount.setVisibility(View.VISIBLE); - if (optionalUser.isPresent()) { - itemViewHolder.binding.uploadAccount.setText( - DisplayUtils.getAccountNameDisplayText(optionalUser.get())); - } else { - itemViewHolder.binding.uploadAccount.setText(item.getAccountName()); - } - } else { - itemViewHolder.binding.uploadAccount.setVisibility(View.GONE); - } - - // Reset fields visibility - itemViewHolder.binding.uploadRemotePath.setVisibility(View.VISIBLE); - itemViewHolder.binding.uploadFileSize.setVisibility(View.VISIBLE); - itemViewHolder.binding.uploadStatus.setVisibility(View.VISIBLE); - itemViewHolder.binding.uploadProgressBar.setVisibility(View.GONE); - - // Update information depending of upload details - String status = getStatusText(item); - switch (item.getUploadStatus()) { - case UPLOAD_IN_PROGRESS -> { - viewThemeUtils.platform.themeHorizontalProgressBar(itemViewHolder.binding.uploadProgressBar); - itemViewHolder.binding.uploadProgressBar.setProgress(0); - itemViewHolder.binding.uploadProgressBar.setVisibility(View.VISIBLE); - - if (uploadHelper.isUploadingNow(item)) { - // really uploading, so... - // ... unbind the old progress bar, if any; ... - if (uploadProgressListener != null) { - final var upload = uploadProgressListener.getUpload(); - if (upload != null) { - String targetKey = FileUploadHelper.Companion.buildRemoteName(upload.getAccountName(), - upload.getRemotePath()); - uploadHelper.removeUploadTransferProgressListener(uploadProgressListener, targetKey); - } - } - // ... then, bind the current progress bar to listen for updates - uploadProgressListener = new UploadProgressListener(item, itemViewHolder.binding.uploadProgressBar); - String targetKey = FileUploadHelper.Companion.buildRemoteName(item.getAccountName(), item.getRemotePath()); - uploadHelper.addUploadTransferProgressListener(uploadProgressListener, targetKey); - - } else { - // not really uploading; stop listening progress if view is reused! - if (uploadProgressListener != null && - uploadProgressListener.isWrapping(itemViewHolder.binding.uploadProgressBar)) { - final var upload = uploadProgressListener.getUpload(); - if (upload != null) { - String targetKey = FileUploadHelper.Companion.buildRemoteName(upload.getAccountName(), - upload.getRemotePath()); - uploadHelper.removeUploadTransferProgressListener(uploadProgressListener, targetKey); - uploadProgressListener = null; - } - } - } - - itemViewHolder.binding.uploadFileSize.setVisibility(View.GONE); - itemViewHolder.binding.uploadProgressBar.invalidate(); - } - case UPLOAD_FAILED -> { - - } - case UPLOAD_SUCCEEDED, UPLOAD_CANCELLED -> - itemViewHolder.binding.uploadStatus.setVisibility(View.GONE); - } - - // show status if same file conflict or local file deleted or upload cancelled - if ((item.getUploadStatus() == UploadStatus.UPLOAD_SUCCEEDED && item.getLastResult() != UploadResult.UPLOADED) - || item.getUploadStatus() == UploadStatus.UPLOAD_CANCELLED) { - - itemViewHolder.binding.uploadStatus.setVisibility(View.VISIBLE); - itemViewHolder.binding.uploadFileSize.setVisibility(View.GONE); - } - - itemViewHolder.binding.uploadStatus.setText(status); - - // bind listeners to perform actions - if (item.getUploadStatus() == UploadStatus.UPLOAD_IN_PROGRESS) { - // Cancel - itemViewHolder.binding.uploadRightButton.setImageResource(R.drawable.ic_action_cancel_grey); - itemViewHolder.binding.uploadRightButton.setVisibility(View.VISIBLE); - itemViewHolder.binding.uploadRightButton.setOnClickListener(v -> { - uploadHelper.updateUploadStatus(item.getRemotePath(), item.getAccountName(), UploadStatus.UPLOAD_CANCELLED, () -> { - FileUploadWorker.Companion.cancelUpload(item.getRemotePath(), item.getAccountName(), () -> { - loadUploadItemsFromDb(() -> {}); - return Unit.INSTANCE; - }); - return Unit.INSTANCE; - }); - }); - - } else if (item.getUploadStatus() == UploadStatus.UPLOAD_FAILED) { - if (item.getLastResult() == UploadResult.SYNC_CONFLICT) { - itemViewHolder.binding.uploadRightButton.setImageResource(R.drawable.ic_dots_vertical); - itemViewHolder.binding.uploadRightButton.setOnClickListener(view -> optionalUser.ifPresent(user -> showItemConflictPopup(user, itemViewHolder, item, status, view))); - } else { - // Delete - itemViewHolder.binding.uploadRightButton.setImageResource(R.drawable.ic_action_delete_grey); - itemViewHolder.binding.uploadRightButton.setOnClickListener(v -> removeUpload(item)); - } - itemViewHolder.binding.uploadRightButton.setVisibility(View.VISIBLE); - } else { // UploadStatus.UPLOAD_SUCCEEDED - itemViewHolder.binding.uploadRightButton.setVisibility(View.INVISIBLE); - } - - itemViewHolder.binding.uploadListItemLayout.setOnClickListener(null); - - // Set icon or thumbnail - itemViewHolder.binding.thumbnail.setImageResource(R.drawable.file); - - // click on item - if (item.getUploadStatus() == UploadStatus.UPLOAD_FAILED || - item.getUploadStatus() == UploadStatus.UPLOAD_CANCELLED) { - - final UploadResult uploadResult = item.getLastResult(); - itemViewHolder.binding.uploadListItemLayout.setOnClickListener(v -> { - if (uploadResult == UploadResult.CREDENTIAL_ERROR) { - final Optional optUser = accountManager.getUser(item.getAccountName()); - final User user = optUser.orElseThrow(RuntimeException::new); - parentActivity.getFileOperationsHelper().checkCurrentCredentials(user); - return; - } else if (uploadResult == UploadResult.SYNC_CONFLICT && optionalUser.isPresent()) { - User user = optionalUser.get(); - if (checkAndOpenConflictResolutionDialog(user, itemViewHolder, item, status)) { - return; - } - } - - // not a credentials error - File file = new File(item.getLocalPath()); - Optional user = accountManager.getUser(item.getAccountName()); - if (file.exists() && user.isPresent()) { - uploadHelper.retryUpload(item, user.get()); - } else { - DisplayUtils.showSnackMessage(v.getRootView().findViewById(android.R.id.content), R.string.local_file_not_found_message); - } - }); - } else if (item.getUploadStatus() == UploadStatus.UPLOAD_SUCCEEDED) { - itemViewHolder.binding.uploadListItemLayout.setOnClickListener(v -> onUploadedItemClick(item)); - } - - - // click on thumbnail to open locally - if (item.getUploadStatus() != UploadStatus.UPLOAD_SUCCEEDED) { - itemViewHolder.binding.thumbnail.setOnClickListener(v -> onUploadingItemClick(item)); - } - - /* - * Cancellation needs do be checked and done before changing the drawable in fileIcon, or - * {@link ThumbnailsCacheManager#cancelPotentialWork} will NEVER cancel any task. - */ - OCFile fakeFileToCheatThumbnailsCacheManagerInterface = new OCFile(item.getRemotePath()); - fakeFileToCheatThumbnailsCacheManagerInterface.setStoragePath(item.getLocalPath()); - fakeFileToCheatThumbnailsCacheManagerInterface.setMimeType(item.getMimeType()); - - boolean allowedToCreateNewThumbnail = ThumbnailsCacheManager.cancelPotentialThumbnailWork( - fakeFileToCheatThumbnailsCacheManagerInterface, itemViewHolder.binding.thumbnail - ); - - // TODO this code is duplicated; refactor to a common place - if (MimeTypeUtil.isImage(fakeFileToCheatThumbnailsCacheManagerInterface) - && fakeFileToCheatThumbnailsCacheManagerInterface.getRemoteId() != null && - item.getUploadStatus() == UploadStatus.UPLOAD_SUCCEEDED) { - // Thumbnail in Cache? - - final var cacheKey = String.valueOf(fakeFileToCheatThumbnailsCacheManagerInterface.getRemoteId()); - Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(cacheKey); - - if (thumbnail != null && !fakeFileToCheatThumbnailsCacheManagerInterface.isUpdateThumbnailNeeded()) { - itemViewHolder.binding.thumbnail.setImageBitmap(thumbnail); - } else { - // generate new Thumbnail - Optional user = parentActivity.getUser(); - if (allowedToCreateNewThumbnail && user.isPresent()) { - final ThumbnailsCacheManager.ThumbnailGenerationTask task = - new ThumbnailsCacheManager.ThumbnailGenerationTask( - itemViewHolder.binding.thumbnail, - parentActivity.getStorageManager(), - user.get() - ); - if (thumbnail == null) { - if (MimeTypeUtil.isVideo(fakeFileToCheatThumbnailsCacheManagerInterface)) { - thumbnail = ThumbnailsCacheManager.mDefaultVideo; - } else { - thumbnail = ThumbnailsCacheManager.mDefaultImg; - } - } - final ThumbnailsCacheManager.AsyncThumbnailDrawable asyncDrawable = - new ThumbnailsCacheManager.AsyncThumbnailDrawable( - parentActivity.getResources(), - thumbnail, - task - ); - itemViewHolder.binding.thumbnail.setImageDrawable(asyncDrawable); - task.execute(new ThumbnailsCacheManager.ThumbnailGenerationTaskObject( - fakeFileToCheatThumbnailsCacheManagerInterface, null)); - } - } - - if ("image/png".equals(item.getMimeType())) { - final var backgroundColor = ContextCompat.getColor(parentActivity, R.color.bg_default); - itemViewHolder.binding.thumbnail.setBackgroundColor(backgroundColor); - } - } else if (MimeTypeUtil.isImage(fakeFileToCheatThumbnailsCacheManagerInterface)) { - File file = new File(item.getLocalPath()); - Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(String.valueOf(file.hashCode())); - - if (thumbnail != null) { - itemViewHolder.binding.thumbnail.setImageBitmap(thumbnail); - } else if (allowedToCreateNewThumbnail) { - getThumbnailFromFileTypeAndSetIcon(item.getLocalPath(), itemViewHolder); - - final ThumbnailsCacheManager.ThumbnailGenerationTask task = - new ThumbnailsCacheManager.ThumbnailGenerationTask(itemViewHolder.binding.thumbnail); - - if (MimeTypeUtil.isVideo(file)) { - thumbnail = ThumbnailsCacheManager.mDefaultVideo; - } else { - thumbnail = ThumbnailsCacheManager.mDefaultImg; - } - - final ThumbnailsCacheManager.AsyncThumbnailDrawable asyncDrawable = - new ThumbnailsCacheManager.AsyncThumbnailDrawable(parentActivity.getResources(), thumbnail, task); - task.execute(new ThumbnailsCacheManager.ThumbnailGenerationTaskObject(file, null)); - task.setListener(new ThumbnailsCacheManager.ThumbnailGenerationTask.Listener() { - @Override - public void onSuccess() { - itemViewHolder.binding.thumbnail.setImageDrawable(asyncDrawable); - } - - @Override - public void onError() { - getThumbnailFromFileTypeAndSetIcon(item.getLocalPath(), itemViewHolder); - } - }); - - Log_OC.v(TAG, "Executing task to generate a new thumbnail"); - } - - if ("image/png".equalsIgnoreCase(item.getMimeType())) { - final var backgroundColor = ContextCompat.getColor(parentActivity, R.color.bg_default); - itemViewHolder.binding.thumbnail.setBackgroundColor(backgroundColor); - } - } else { - if (optionalUser.isPresent()) { - final Drawable icon = MimeTypeUtil.getFileTypeIcon(item.getMimeType(), - fileName, - parentActivity, - viewThemeUtils); - itemViewHolder.binding.thumbnail.setImageDrawable(icon); - } - } - } - - private void getThumbnailFromFileTypeAndSetIcon(String localPath, ItemViewHolder itemViewHolder) { - Drawable drawable = MimeTypeUtil.getIcon(localPath, parentActivity, viewThemeUtils); - if (drawable == null) return; - itemViewHolder.binding.thumbnail.setImageDrawable(drawable); - } - - private boolean checkAndOpenConflictResolutionDialog(User user, - ItemViewHolder itemViewHolder, - OCUpload item, - String status) { - String remotePath = item.getRemotePath(); - OCFile localFile = storageManager.getFileByEncryptedRemotePath(remotePath); - - if (localFile == null) { - // Remote file doesn't exist, try to refresh folder - OCFile folder = storageManager.getFileByEncryptedRemotePath(new 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) { - this.openConflictActivity(localFile, item); - return true; - } - - // Remote file doesn't exist anymore = there is no more conflict - return false; - } - - private void refreshFolderAndUpdateUI(ItemViewHolder holder, User user, OCFile folder, String remotePath, - OCUpload item, String status) { - Context context = MainApp.getAppContext(); - - this.refreshFolder(context, holder, user, folder, (caller, result) -> { - holder.binding.uploadStatus.setText(status); - - if (result.isSuccess()) { - OCFile fileOnServer = storageManager.getFileByEncryptedRemotePath(remotePath); - - if (fileOnServer != null) { - openConflictActivity(fileOnServer, item); - } else { - displayFileNotFoundError(holder.itemView, context); - } - } - }); - } - - private void displayFileNotFoundError(View itemView, Context context) { - String message = context.getString(R.string.uploader_file_not_found_message); - DisplayUtils.showSnackMessage(itemView, message); - } - - private void showItemConflictPopup(User user, - ItemViewHolder itemViewHolder, - OCUpload item, - String status, - View view) { - PopupMenu popup = new PopupMenu(MainApp.getAppContext(), view); - popup.inflate(R.menu.upload_list_item_file_conflict); - popup.setOnMenuItemClickListener(i -> { - int itemId = i.getItemId(); - - if (itemId == R.id.action_upload_list_resolve_conflict) { - checkAndOpenConflictResolutionDialog(user, itemViewHolder, item, status); - } else { - removeUpload(item); - } - - return true; - }); - popup.show(); - } - - public void removeUpload(OCUpload item) { - uploadsStorageManager.removeUpload(item); - cancelOldErrorNotification(item); - loadUploadItemsFromDb(() -> {}); - } - - private void refreshFolder( - Context context, - ItemViewHolder view, - User user, - OCFile folder, - OnRemoteOperationListener listener) { - view.binding.uploadListItemLayout.setClickable(false); - view.binding.uploadStatus.setText(R.string.uploads_view_upload_status_fetching_server_version); - new RefreshFolderOperation(folder, - clock.getCurrentTime(), - false, - false, - true, - storageManager, - user, - context) - .execute(user, context, (caller, result) -> { - view.binding.uploadListItemLayout.setClickable(true); - listener.onRemoteOperationFinish(caller, result); - }, parentActivity.getHandler()); - } - - private void openConflictActivity(OCFile file, OCUpload upload) { - file.setStoragePath(upload.getLocalPath()); - - Context context = MainApp.getAppContext(); - Optional user = accountManager.getUser(upload.getAccountName()); - if (user.isPresent()) { - Intent intent = ConflictsResolveActivity.createIntent(file, - user.get(), - upload.getUploadId(), - Intent.FLAG_ACTIVITY_NEW_TASK, - context); - - context.startActivity(intent); - } - } - - /** - * Gets the status text to show to the user according to the status and last result of the the given upload. - * - * @param upload Upload to describe. - * @return Text describing the status of the given upload. - */ - private String getStatusText(OCUpload upload) { - String status; - var statusRes = parentActivity.getResources(); - var prefs = parentActivity.getAppPreferences(); - var uploadStatus = upload.getUploadStatus(); - - switch (uploadStatus) { - case UPLOAD_IN_PROGRESS -> { - if (prefs.isGlobalUploadPaused()) { - status = statusRes.getString(R.string.upload_global_pause_title); - } else if (uploadHelper.isUploadingNow(upload)) { - status = statusRes.getString(R.string.uploader_upload_in_progress_ticker); - } else { - status = statusRes.getString(R.string.uploads_view_later_waiting_to_upload); - } - } - - case UPLOAD_SUCCEEDED -> { - UploadResult result = upload.getLastResult(); - if (result == UploadResult.SAME_FILE_CONFLICT) { - status = statusRes.getString(R.string.uploads_view_upload_status_succeeded_same_file); - } else if (result == UploadResult.FILE_NOT_FOUND) { - status = getUploadFailedStatusText(result); - } else if (upload.getNameCollisionPolicy() == NameCollisionPolicy.SKIP) { - status = statusRes.getString(R.string.uploads_view_upload_status_skip_reason); - } else { - status = statusRes.getString(R.string.uploads_view_upload_status_succeeded); - } - } - - case UPLOAD_FAILED -> - status = getUploadFailedStatusText(upload.getLastResult()); - - case UPLOAD_CANCELLED -> - status = statusRes.getString(R.string.upload_manually_cancelled); - - default -> - status = "Uncontrolled status: " + uploadStatus; - } - - return status; - } - - - @NonNull - private String getUploadFailedStatusText(UploadResult result) { - return switch (result) { - case CREDENTIAL_ERROR -> - parentActivity.getString(R.string.uploads_view_upload_status_failed_credentials_error); - case FOLDER_ERROR -> parentActivity.getString(R.string.uploads_view_upload_status_failed_folder_error); - case FILE_NOT_FOUND -> parentActivity.getString(R.string.uploads_view_upload_status_failed_localfile_error); - case FILE_ERROR -> parentActivity.getString(R.string.uploads_view_upload_status_failed_file_error); - case PRIVILEGES_ERROR -> - parentActivity.getString(R.string.uploads_view_upload_status_failed_permission_error); - case NETWORK_CONNECTION -> - parentActivity.getString(R.string.uploads_view_upload_status_failed_connection_error); - case DELAYED_FOR_WIFI -> parentActivity.getString(R.string.uploads_view_upload_status_waiting_for_wifi); - case DELAYED_FOR_CHARGING -> - parentActivity.getString(R.string.uploads_view_upload_status_waiting_for_charging); - case CONFLICT_ERROR -> parentActivity.getString(R.string.uploads_view_upload_status_conflict); - case SERVICE_INTERRUPTED -> - parentActivity.getString(R.string.uploads_view_upload_status_service_interrupted); - case CANCELLED -> - // should not get here ; cancelled uploads should be wiped out - parentActivity.getString(R.string.uploads_view_upload_status_cancelled); - case UPLOADED -> - // should not get here ; status should be UPLOAD_SUCCESS - parentActivity.getString(R.string.uploads_view_upload_status_succeeded); - case MAINTENANCE_MODE -> parentActivity.getString(R.string.maintenance_mode); - case SSL_RECOVERABLE_PEER_UNVERIFIED -> parentActivity.getString( - R.string.uploads_view_upload_status_failed_ssl_certificate_not_trusted - ); - case UNKNOWN -> parentActivity.getString(R.string.uploads_view_upload_status_unknown_fail); - case LOCK_FAILED -> parentActivity.getString(R.string.upload_lock_failed); - case DELAYED_IN_POWER_SAVE_MODE -> parentActivity.getString( - R.string.uploads_view_upload_status_waiting_exit_power_save_mode); - case VIRUS_DETECTED -> parentActivity.getString(R.string.uploads_view_upload_status_virus_detected); - case LOCAL_STORAGE_FULL -> parentActivity.getString(R.string.upload_local_storage_full); - case OLD_ANDROID_API -> parentActivity.getString(R.string.upload_old_android); - case SYNC_CONFLICT -> parentActivity.getString(R.string.upload_sync_conflict); - case CANNOT_CREATE_FILE -> parentActivity.getString(R.string.upload_cannot_create_file); - case LOCAL_STORAGE_NOT_COPIED -> parentActivity.getString(R.string.upload_local_storage_not_copied); - case QUOTA_EXCEEDED -> parentActivity.getString(R.string.upload_quota_exceeded); - default -> parentActivity.getString(R.string.upload_unknown_error); - }; - } - - @Override - @NonNull - public SectionedViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - if (viewType == VIEW_TYPE_HEADER) { - return new HeaderViewHolder( - UploadListHeaderBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false) - ); - } else { - return new ItemViewHolder( - UploadListItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false) - ); - } - } - - @SuppressLint("NotifyDataSetChanged") - public final void loadUploadItemsFromDb(Runnable onCompleted) { - parentActivity.getUser().ifPresent(user -> { - String accountName = user.getAccountName(); - final var optionalCapabilities = parentActivity.getCapabilities(); - if (optionalCapabilities.isPresent()) { - final var capabilities = optionalCapabilities.get(); - for (int i = 0; i < sections.size(); i++) { - final int index = i; - Section sec = sections.get(index); - - uploadHelper.getUploadsByStatus(accountName, - sec.status(), - capabilities, - sec.collisionPolicy(), - uploads -> { - for (OCUpload upload : uploads) { - upload.setDataFixed(uploadHelper); - } - Arrays.sort(uploads, new OCUploadComparator()); - - sections.set(index, sec.withItems(uploads)); - - parentActivity.runOnUiThread(() -> { - notifyDataSetChanged(); - onCompleted.run(); - }); - return Unit.INSTANCE; - }); - } - } - }); - } - - /** - * Open local file. - */ - private void onUploadingItemClick(OCUpload file) { - File f = new File(file.getLocalPath()); - if (!f.exists()) { - DisplayUtils.showSnackMessage(parentActivity, R.string.local_file_not_found_message); - } else { - openFileWithDefault(file.getLocalPath()); - } - } - - /** - * Open remote file. - */ - private void onUploadedItemClick(OCUpload upload) { - final OCFile file = parentActivity.getStorageManager().getFileByEncryptedRemotePath(upload.getRemotePath()); - if (file == null) { - DisplayUtils.showSnackMessage(parentActivity, R.string.error_retrieving_file); - Log_OC.i(TAG, "Could not find uploaded file on remote."); - return; - } - - final var optionalUser = parentActivity.getUser(); - - if (PreviewImageFragment.canBePreviewed(file) && optionalUser.isPresent()) { - //show image preview and stay in uploads tab - Intent intent = FileDisplayActivity.openFileIntent(parentActivity, optionalUser.get(), file); - parentActivity.startActivity(intent); - } else { - Intent intent = new Intent(parentActivity, FileDisplayActivity.class); - intent.setAction(Intent.ACTION_VIEW); - intent.putExtra(FileDisplayActivity.KEY_FILE_PATH, upload.getRemotePath()); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - parentActivity.startActivity(intent); - } - } - - - /** - * Open file with app associates with its MIME type. If MIME type unknown, show list with all apps. - */ - private void openFileWithDefault(String localPath) { - Intent myIntent = new Intent(Intent.ACTION_VIEW); - File file = new File(localPath); - String mimetype = MimeTypeUtil.getBestMimeTypeByFilename(localPath); - if ("application/octet-stream".equals(mimetype)) { - mimetype = "*/*"; - } - myIntent.setDataAndType(Uri.fromFile(file), mimetype); - try { - parentActivity.startActivity(myIntent); - } catch (ActivityNotFoundException e) { - DisplayUtils.showSnackMessage(parentActivity, R.string.file_list_no_app_for_file_type); - Log_OC.i(TAG, "Could not find app for sending log history."); - } - } - - static class HeaderViewHolder extends SectionedViewHolder { - UploadListHeaderBinding binding; - - HeaderViewHolder(UploadListHeaderBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } - } - - static class ItemViewHolder extends SectionedViewHolder { - UploadListItemBinding binding; - - ItemViewHolder(UploadListItemBinding binding) { - super(binding.getRoot()); - this.binding = binding; - } - } - - enum Type { - CURRENT, COMPLETED, FAILED, CANCELLED, SKIPPED - } - - public void cancelOldErrorNotification(OCUpload upload) { - if (mNotificationManager == null) { - mNotificationManager = (NotificationManager) parentActivity.getSystemService(Context.NOTIFICATION_SERVICE); - } - - if (upload == null) { - return; - } - - mNotificationManager.cancel((int) upload.getUploadId()); - } -} 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 new file mode 100644 index 000000000000..248bb46e4c9a --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapter.kt @@ -0,0 +1,780 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.adapter.uploadList + +import android.annotation.SuppressLint +import android.app.NotificationManager +import android.content.Context +import android.text.format.DateUtils +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.PopupMenu +import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope +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 +import com.nextcloud.client.network.ConnectivityService +import com.nextcloud.utils.extensions.getStatusText +import com.nextcloud.utils.extensions.setVisibleIf +import com.nextcloud.utils.extensions.sortedByUploadOrder +import com.nextcloud.utils.extensions.toFile +import com.owncloud.android.R +import com.owncloud.android.databinding.UploadListHeaderBinding +import com.owncloud.android.databinding.UploadListItemBinding +import com.owncloud.android.datamodel.FileDataStorageManager +import com.owncloud.android.datamodel.OCFile +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.utils.DisplayUtils +import com.owncloud.android.utils.MimeTypeUtil +import com.owncloud.android.utils.theme.ViewThemeUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.File +import java.util.Optional +import java.util.function.Consumer + +@Suppress( + "LongMethod", + "TooManyFunctions", + "LargeClass", + "LongParameterList", + "NestedBlockDepth", + "MaxLineLength", + "ReturnCount" +) +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 +) : SectionedRecyclerViewAdapter() { + + private val uploadListSections = UploadListSection.sections() + private val showUser: Boolean = accountManager.getAccounts().size > 1 + 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") + shouldShowHeadersForEmptySections(false) + } + + internal class HeaderViewHolder(val binding: UploadListHeaderBinding) : SectionedViewHolder(binding.root) + + internal class ItemViewHolder(val binding: UploadListItemBinding) : SectionedViewHolder(binding.root) + + override fun getSectionCount(): Int = uploadListSections.size + + override fun getItemCount(section: Int): Int = uploadListSections[section].items.size + + // region header + override fun onBindHeaderViewHolder(holder: SectionedViewHolder, section: Int, expanded: Boolean) { + val headerViewHolder = holder as HeaderViewHolder + val group = uploadListSections[section] + + bindHeaderTitle(headerViewHolder, group, section) + bindHeaderActionButton(headerViewHolder, group) + bindHeaderBatterySaverWarning(headerViewHolder) + bindHeaderActionClickListener(headerViewHolder, group) + } + + private fun bindHeaderTitle(holder: HeaderViewHolder, group: UploadListSection, section: Int) { + val title = activity.getString(group.titleRes) + val headerText = activity.getString(R.string.uploads_view_group_header) + holder.binding.uploadListTitle.text = String.format(headerText, title, group.items.size) + viewThemeUtils.platform.colorTextView(holder.binding.uploadListTitle) + + val toggleExpand = { + toggleSectionExpanded(section) + val icon = if (isSectionExpanded(section)) R.drawable.ic_expand_less else R.drawable.ic_expand_more + holder.binding.uploadListState.setImageResource(icon) + } + holder.binding.uploadListTitle.setOnClickListener { toggleExpand() } + holder.binding.uploadListStateLayout.setOnClickListener { toggleExpand() } + } + + private fun bindHeaderActionButton(holder: HeaderViewHolder, group: UploadListSection) { + val iconRes = when (group.type) { + UploadListType.CURRENT, UploadListType.COMPLETED -> R.drawable.ic_close + UploadListType.CANCELLED, UploadListType.FAILED -> R.drawable.ic_dots_vertical + else -> return + } + holder.binding.uploadListAction.setImageResource(iconRes) + } + + private fun bindHeaderBatterySaverWarning(holder: HeaderViewHolder) { + holder.binding.autoUploadBatterySaverWarningCard.root + .setVisibleIf(powerManagementService.isPowerSavingEnabled) + viewThemeUtils.material.themeCardView(holder.binding.autoUploadBatterySaverWarningCard.root) + } + + private fun bindHeaderActionClickListener(holder: HeaderViewHolder, group: UploadListSection) { + holder.binding.uploadListAction.setOnClickListener { + when (group.type) { + UploadListType.CURRENT -> cancelAllCurrentUploads(group) + + UploadListType.COMPLETED -> { + uploadsStorageManager.clearSuccessfulUploads() + loadUploadItemsFromDb() + } + + UploadListType.FAILED -> showFailedPopupMenu(holder) + + UploadListType.CANCELLED -> showCancelledPopupMenu(holder) + + else -> {} + } + } + } + + private fun cancelAllCurrentUploads(group: UploadListSection) { + val items = group.items.takeIf { it.isNotEmpty() } ?: return + val accountName = items[0].accountName + var completedCount = 0 + items.forEach { upload -> + uploadHelper.updateUploadStatus( + upload.remotePath, + accountName, + UploadsStorageManager.UploadStatus.UPLOAD_CANCELLED + ) { + FileUploadWorker.cancelUpload(upload.remotePath, accountName) { + completedCount++ + if (completedCount == items.size) { + Log_OC.d(TAG, "refreshing upload items") + loadUploadItemsFromDb() + } + } + } + } + } + + private fun showFailedPopupMenu(holder: HeaderViewHolder) { + PopupMenu(activity, holder.binding.uploadListAction).apply { + inflate(R.menu.upload_list_failed_options) + setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.action_upload_list_failed_clear -> { + uploadsStorageManager.clearFailedButNotDelayedUploads() + clearTempEncryptedFolder() + loadUploadItemsFromDb() + } + + R.id.action_upload_list_failed_retry -> + uploadHelper.retryFailedUploads( + uploadsStorageManager, + connectivityService, + accountManager, + powerManagementService + ) + } + true + } + show() + } + } + + private fun showCancelledPopupMenu(holder: HeaderViewHolder) { + PopupMenu(activity, holder.binding.uploadListAction).apply { + inflate(R.menu.upload_list_cancelled_options) + setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.action_upload_list_cancelled_clear -> { + uploadsStorageManager.clearCancelledUploadsForCurrentAccount() + loadUploadItemsFromDb() + clearTempEncryptedFolder() + } + + R.id.action_upload_list_cancelled_resume -> retryCancelledUploads() + } + true + } + show() + } + } + + private fun clearTempEncryptedFolder() { + val user = activity.user + user.ifPresent( + Consumer { value: User? -> + FileDataStorageManager.clearTempEncryptedFolder(value!!.accountName) + } + ) + } + + // FIXME For e2e resume is not working + private fun retryCancelledUploads() { + activity.lifecycleScope.launch(Dispatchers.IO) { + val showNotExistMessage = uploadHelper.retryCancelledUploads( + uploadsStorageManager, + connectivityService, + accountManager, + powerManagementService + ) + if (showNotExistMessage) { + withContext(Dispatchers.Main) { + DisplayUtils.showSnackMessage(activity, R.string.upload_action_file_not_exist_message) + } + } + } + } + // endregion + + // region content + override fun onBindViewHolder( + holder: SectionedViewHolder?, + section: Int, + relativePosition: Int, + absolutePosition: Int + ) { + if (uploadListSections.isEmpty() || section !in uploadListSections.indices) return + val item = uploadListSections[section].items[relativePosition] + val itemViewHolder = holder as ItemViewHolder + + bindItemText(holder, item) + bindItemStatus(itemViewHolder, item) + bindItemActions(itemViewHolder, item) + bindItemThumbnail(itemViewHolder, item) + } + + @SuppressLint("SetTextI18n") + private fun bindItemText(holder: ItemViewHolder, item: OCUpload) { + val remoteFile = File(item.remotePath) + val fileName = remoteFile.name.takeIf { it.isNotEmpty() } ?: File.separator + holder.binding.uploadName.text = fileName + holder.binding.uploadRemotePath.text = File(item.remotePath).parent + + val updateTime = item.uploadEndTimestamp + if (item.fileSize != 0L) { + var fileSizeFormat = "%s " + + // we have valid update time so we can show the upload date + if (updateTime > 0) { + fileSizeFormat = "%s, " + } + + val fileSizeInBytes = DisplayUtils.bytesToHumanReadable(item.fileSize) + val uploadFileSize = String.format(fileSizeFormat, fileSizeInBytes) + holder.binding.uploadFileSize.text = uploadFileSize + } else { + holder.binding.uploadFileSize.text = "" + } + + bindItemDate(holder, item, updateTime) + bindItemAccount(holder, item) + } + + private fun bindItemDate(holder: ItemViewHolder, item: OCUpload, updateTime: Long) { + val showDate = ( + updateTime > 0 && + item.uploadStatus == UploadsStorageManager.UploadStatus.UPLOAD_SUCCEEDED && + item.lastResult == UploadResult.UPLOADED + ) + + holder.binding.uploadDate.setVisibleIf(showDate) + + if (showDate) { + holder.binding.uploadDate.text = DisplayUtils.getRelativeDateTimeString( + activity, + updateTime, + DateUtils.MINUTE_IN_MILLIS, + DateUtils.WEEK_IN_MILLIS, + 0 + ) + } + } + + private fun bindItemAccount(holder: ItemViewHolder, item: OCUpload) { + if (showUser) { + holder.binding.uploadAccount.visibility = View.VISIBLE + val optionalUser = accountManager.getUser(item.accountName) + holder.binding.uploadAccount.text = if (optionalUser.isPresent) { + DisplayUtils.getAccountNameDisplayText(optionalUser.get()) + } else { + item.accountName + } + } else { + holder.binding.uploadAccount.visibility = View.GONE + } + } + + private fun bindItemStatus(holder: ItemViewHolder, item: OCUpload) { + holder.binding.run { + uploadRemotePath.visibility = View.VISIBLE + uploadFileSize.visibility = View.VISIBLE + uploadStatus.visibility = View.VISIBLE + uploadProgressBar.visibility = View.GONE + + val status = item.getStatusText( + activity, + activity.appPreferences.isGlobalUploadPaused, + uploadHelper.isUploadingNow(item) + ) + when (item.uploadStatus) { + UploadsStorageManager.UploadStatus.UPLOAD_IN_PROGRESS -> bindItemInProgress(holder, item) + + UploadsStorageManager.UploadStatus.UPLOAD_SUCCEEDED, + UploadsStorageManager.UploadStatus.UPLOAD_CANCELLED -> uploadStatus.visibility = View.GONE + + else -> {} + } + + // Override visibility for edge cases + if (( + item.uploadStatus == UploadsStorageManager.UploadStatus.UPLOAD_SUCCEEDED && + item.lastResult != UploadResult.UPLOADED + ) || + item.uploadStatus == UploadsStorageManager.UploadStatus.UPLOAD_CANCELLED + ) { + uploadStatus.visibility = View.VISIBLE + uploadFileSize.visibility = View.GONE + } + + uploadStatus.text = status + } + } + + private fun bindItemInProgress(holder: ItemViewHolder, item: OCUpload) { + holder.binding.run { + viewThemeUtils.platform.themeHorizontalProgressBar(uploadProgressBar) + uploadProgressBar.progress = 0 + uploadProgressBar.visibility = View.VISIBLE + uploadFileSize.visibility = View.GONE + + if (uploadHelper.isUploadingNow(item)) { + uploadProgressListener?.upload?.let { prevUpload -> + val key = FileUploadHelper.buildRemoteName(prevUpload.accountName, prevUpload.remotePath) + uploadHelper.removeUploadTransferProgressListener(uploadProgressListener!!, key) + } + uploadProgressListener = UploadProgressListener(item, uploadProgressBar) + uploadHelper.addUploadTransferProgressListener( + uploadProgressListener!!, + FileUploadHelper.buildRemoteName(item.accountName, item.remotePath) + ) + } else if (uploadProgressListener?.isWrapping(uploadProgressBar) == true) { + uploadProgressListener?.upload?.let { prevUpload -> + val key = FileUploadHelper.buildRemoteName(prevUpload.accountName, prevUpload.remotePath) + uploadHelper.removeUploadTransferProgressListener(uploadProgressListener!!, key) + uploadProgressListener = null + } + } + + uploadProgressBar.invalidate() + } + } + + 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) { + UploadsStorageManager.UploadStatus.UPLOAD_IN_PROGRESS -> { + uploadRightButton.run { + setImageResource(R.drawable.ic_action_cancel_grey) + visibility = View.VISIBLE + setOnClickListener { + uploadHelper.updateUploadStatus( + item.remotePath, + item.accountName, + UploadsStorageManager.UploadStatus.UPLOAD_CANCELLED + ) { + FileUploadWorker.cancelUpload( + item.remotePath, + item.accountName + ) { loadUploadItemsFromDb() } + } + } + } + } + + UploadsStorageManager.UploadStatus.UPLOAD_FAILED -> { + uploadRightButton.run { + if (item.lastResult == UploadResult.SYNC_CONFLICT) { + setImageResource(R.drawable.ic_dots_vertical) + setOnClickListener { view -> + optionalUser.ifPresent { user -> + showItemConflictPopup(user, holder, item, status, view) + } + } + } else { + setImageResource(R.drawable.ic_action_delete_grey) + setOnClickListener { removeUpload(item) } + } + visibility = View.VISIBLE + } + } + + else -> uploadRightButton.visibility = View.INVISIBLE + } + + // Row click + uploadListItemLayout.run { + setOnClickListener(null) + when (item.uploadStatus) { + UploadsStorageManager.UploadStatus.UPLOAD_FAILED, + UploadsStorageManager.UploadStatus.UPLOAD_CANCELLED -> + setOnClickListener { + onFailedOrCancelledItemClick(item, optionalUser, holder, status) + } + + UploadsStorageManager.UploadStatus.UPLOAD_SUCCEEDED -> + setOnClickListener { helper.onUploadedItemClick(item) } + + else -> {} + } + } + + // Thumbnail click to open locally + if (item.uploadStatus != UploadsStorageManager.UploadStatus.UPLOAD_SUCCEEDED) { + thumbnail.setOnClickListener { helper.onUploadingItemClick(item) } + } + } + } + + private fun onFailedOrCancelledItemClick( + item: OCUpload, + optionalUser: Optional, + holder: ItemViewHolder, + status: String + ) { + when (item.lastResult) { + UploadResult.CREDENTIAL_ERROR -> { + val user = optionalUser.orElseThrow { RuntimeException() } + activity.fileOperationsHelper.checkCurrentCredentials(user) + } + + UploadResult.SYNC_CONFLICT if optionalUser.isPresent -> { + if (checkAndOpenConflictResolutionDialog(optionalUser.get(), holder, item, status)) return + retryOrShowError(item) + } + + else -> retryOrShowError(item) + } + } + + private fun retryOrShowError(item: OCUpload) { + val user = accountManager.getUser(item.accountName) + if (user.isEmpty) return + + activity.lifecycleScope.launch(Dispatchers.IO) { + val file = item.localPath.toFile() + + withContext(Dispatchers.Main) { + if (file != null) { + uploadHelper.retryUpload(item, user.get()) + } else { + DisplayUtils.showSnackMessage( + activity, + R.string.local_file_not_found_message + ) + } + } + } + } + + private fun bindItemThumbnail(holder: ItemViewHolder, item: OCUpload) { + holder.binding.thumbnail.setImageResource(R.drawable.file) + + val fakeFile = OCFile(item.remotePath).apply { + setStoragePath(item.localPath) + mimeType = item.mimeType + } + + val allowedToCreateNewThumbnail = + ThumbnailsCacheManager.cancelPotentialThumbnailWork(fakeFile, holder.binding.thumbnail) + + val optionalUser = accountManager.getUser(item.accountName) + val fileName = File(item.remotePath).name.takeIf { it.isNotEmpty() } ?: File.separator + + when { + MimeTypeUtil.isImage(fakeFile) && fakeFile.remoteId != null && + item.uploadStatus == UploadsStorageManager.UploadStatus.UPLOAD_SUCCEEDED -> + bindRemoteThumbnail(holder, item, fakeFile, allowedToCreateNewThumbnail) + + MimeTypeUtil.isImage(fakeFile) -> + bindLocalThumbnail(holder, item, allowedToCreateNewThumbnail) + + optionalUser.isPresent -> { + val icon = MimeTypeUtil.getFileTypeIcon(item.mimeType, fileName, activity, viewThemeUtils) + holder.binding.thumbnail.setImageDrawable(icon) + } + } + } + + private fun bindRemoteThumbnail( + holder: ItemViewHolder, + item: OCUpload, + fakeFile: OCFile, + allowedToCreateNewThumbnail: Boolean + ) { + val cacheKey = fakeFile.remoteId.toString() + var thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(cacheKey) + + if (thumbnail != null && !fakeFile.isUpdateThumbnailNeeded) { + holder.binding.thumbnail.setImageBitmap(thumbnail) + } else if (allowedToCreateNewThumbnail) { + val user = activity.user + if (user.isPresent) { + val task = ThumbnailsCacheManager.ThumbnailGenerationTask( + holder.binding.thumbnail, + activity.storageManager, + user.get() + ) + thumbnail = thumbnail ?: if (MimeTypeUtil.isVideo(fakeFile)) { + ThumbnailsCacheManager.mDefaultVideo + } else { + ThumbnailsCacheManager.mDefaultImg + } + holder.binding.thumbnail.setImageDrawable( + ThumbnailsCacheManager.AsyncThumbnailDrawable(activity.resources, thumbnail, task) + ) + task.execute(ThumbnailsCacheManager.ThumbnailGenerationTaskObject(fakeFile, null)) + } + } + + if (item.mimeType == "image/png") { + holder.binding.thumbnail.setBackgroundColor(ContextCompat.getColor(activity, R.color.bg_default)) + } + } + + private fun bindLocalThumbnail(holder: ItemViewHolder, item: OCUpload, allowedToCreateNewThumbnail: Boolean) { + val file = File(item.localPath) + val thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(file.hashCode().toString()) + + if (thumbnail != null) { + holder.binding.thumbnail.setImageBitmap(thumbnail) + } else if (allowedToCreateNewThumbnail) { + getThumbnailFromFileTypeAndSetIcon(item.localPath, holder) + val task = ThumbnailsCacheManager.ThumbnailGenerationTask(holder.binding.thumbnail) + val defaultThumbnail = if (MimeTypeUtil.isVideo(file)) { + ThumbnailsCacheManager.mDefaultVideo + } else { + ThumbnailsCacheManager.mDefaultImg + } + val asyncDrawable = + ThumbnailsCacheManager.AsyncThumbnailDrawable(activity.resources, defaultThumbnail, task) + task.execute(ThumbnailsCacheManager.ThumbnailGenerationTaskObject(file, null)) + task.setListener(object : ThumbnailsCacheManager.ThumbnailGenerationTask.Listener { + override fun onSuccess() { + holder.binding.thumbnail.setImageDrawable(asyncDrawable) + } + + override fun onError() { + getThumbnailFromFileTypeAndSetIcon(item.localPath, holder) + } + }) + Log_OC.v(TAG, "Executing task to generate a new thumbnail") + } + + if (item.mimeType.equals("image/png", ignoreCase = true)) { + holder.binding.thumbnail.setBackgroundColor(ContextCompat.getColor(activity, R.color.bg_default)) + } + } + // endregion + + override fun onBindFooterViewHolder(holder: SectionedViewHolder?, section: Int) = Unit + + private fun getThumbnailFromFileTypeAndSetIcon(localPath: String?, itemViewHolder: ItemViewHolder) { + val drawable = MimeTypeUtil.getIcon(localPath, activity, viewThemeUtils) ?: return + 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? + ) { + 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) + } else { + removeUpload(item) + } + true + } + show() + } + } + + fun removeUpload(item: OCUpload?) { + uploadsStorageManager.removeUpload(item) + cancelOldErrorNotification(item) + 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( + UploadListHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + } else { + ItemViewHolder( + UploadListItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + } + + @SuppressLint("NotifyDataSetChanged") + @JvmOverloads + fun loadUploadItemsFromDb(onCompleted: Runnable = {}) { + val optionalUser = activity.user + val optionalCapabilities = activity.capabilities + if (optionalUser.isEmpty || optionalCapabilities.isEmpty) return + + val accountName = optionalUser.get().accountName + val capabilities = optionalCapabilities.get() + + activity.lifecycleScope.launch(Dispatchers.IO) { + val updatedSections = uploadListSections.map { sec -> + val uploads = uploadHelper.getUploadsByStatus( + accountName, + sec.status, + capabilities, + sec.collisionPolicy + ) + uploads.forEach { it.setDataFixed(uploadHelper) } + sec.withItems(uploads.sortedByUploadOrder()) + } + + withContext(Dispatchers.Main) { + for (i in uploadListSections.indices) { + uploadListSections[i] = updatedSections[i] + } + notifyDataSetChanged() + onCompleted.run() + } + } + } + + fun cancelOldErrorNotification(upload: OCUpload?) { + if (notificationManager == null) { + notificationManager = activity.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager? + } + + if (upload == null) { + return + } + + notificationManager?.cancel(upload.uploadId.toInt()) + } + + companion object { + private val TAG: String = UploadListAdapter::class.java.getSimpleName() + } +} 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/UploadListAdapterHelper.kt new file mode 100644 index 000000000000..64cd8edb56c0 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListAdapterHelper.kt @@ -0,0 +1,93 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.adapter.uploadList + +import android.content.ActivityNotFoundException +import android.content.Intent +import android.net.Uri +import com.owncloud.android.R +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.db.OCUpload +import com.owncloud.android.lib.common.utils.Log_OC +import com.owncloud.android.ui.activity.ConflictsResolveActivity +import com.owncloud.android.ui.activity.FileActivity +import com.owncloud.android.ui.activity.FileDisplayActivity +import com.owncloud.android.ui.preview.PreviewImageFragment +import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.MimeTypeUtil +import java.io.File + +class UploadListAdapterHelper(private val activity: FileActivity) { + + companion object { + private const val TAG = "UploadListAdapterHelper" + } + + fun openConflictActivity(file: OCFile, upload: OCUpload) { + file.setStoragePath(upload.localPath) + val user = activity.accountManager.getUser(upload.accountName) + if (user.isPresent) { + val intent = ConflictsResolveActivity.createIntent( + file, + user.get(), + upload.uploadId, + Intent.FLAG_ACTIVITY_NEW_TASK, + activity + ) + activity.startActivity(intent) + } + } + + fun onUploadingItemClick(file: OCUpload) { + val f = File(file.localPath) + if (!f.exists()) { + DisplayUtils.showSnackMessage(activity, R.string.local_file_not_found_message) + } else { + openFileWithDefault(file.localPath) + } + } + + fun onUploadedItemClick(upload: OCUpload) { + val file = activity.storageManager.getFileByEncryptedRemotePath(upload.remotePath) + if (file == null) { + DisplayUtils.showSnackMessage(activity, R.string.error_retrieving_file) + Log_OC.i(TAG, "Could not find uploaded file on remote.") + return + } + + val optionalUser = activity.user + if (PreviewImageFragment.canBePreviewed(file) && optionalUser.isPresent) { + // show image preview and stay in uploads tab + val intent = FileDisplayActivity.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) + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + } + activity.startActivity(intent) + } + + fun openFileWithDefault(localPath: String) { + var mimetype = MimeTypeUtil.getBestMimeTypeByFilename(localPath) + if (mimetype == "application/octet-stream") mimetype = "*/*" + try { + activity.startActivity( + Intent(Intent.ACTION_VIEW).apply { + setDataAndType(Uri.fromFile(File(localPath)), mimetype) + } + ) + } catch (e: ActivityNotFoundException) { + DisplayUtils.showSnackMessage(activity, R.string.file_list_no_app_for_file_type) + Log_OC.i(TAG, "Could not find app for sending log history: $e") + } + } +} 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/UploadListSection.kt new file mode 100644 index 000000000000..6cb1bcc96995 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListSection.kt @@ -0,0 +1,63 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.adapter.uploadList + +import com.owncloud.android.R +import com.owncloud.android.datamodel.UploadsStorageManager +import com.owncloud.android.db.OCUpload +import com.owncloud.android.files.services.NameCollisionPolicy + +data class UploadListSection( + val type: UploadListType?, + val titleRes: Int, + val status: UploadsStorageManager.UploadStatus, + val collisionPolicy: NameCollisionPolicy?, + val items: List +) { + fun withItems(newItems: List) = copy(items = newItems) + + companion object { + fun sections(): MutableList = mutableListOf( + UploadListSection( + UploadListType.CURRENT, + R.string.uploads_view_group_current_uploads, + UploadsStorageManager.UploadStatus.UPLOAD_IN_PROGRESS, + null, + listOf() + ), + UploadListSection( + UploadListType.FAILED, + R.string.uploads_view_group_failed_uploads, + UploadsStorageManager.UploadStatus.UPLOAD_FAILED, + null, + listOf() + ), + UploadListSection( + UploadListType.CANCELLED, + R.string.uploads_view_group_manually_cancelled_uploads, + UploadsStorageManager.UploadStatus.UPLOAD_CANCELLED, + null, + listOf() + ), + UploadListSection( + UploadListType.COMPLETED, + R.string.uploads_view_group_completed_uploads, + UploadsStorageManager.UploadStatus.UPLOAD_SUCCEEDED, + NameCollisionPolicy.ASK_USER, + listOf() + ), + UploadListSection( + UploadListType.SKIPPED, + R.string.uploads_view_upload_status_skip, + UploadsStorageManager.UploadStatus.UPLOAD_SUCCEEDED, + NameCollisionPolicy.SKIP, + listOf() + ) + ) + } +} 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/UploadListType.kt new file mode 100644 index 000000000000..aa2d818f9378 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/adapter/uploadList/UploadListType.kt @@ -0,0 +1,10 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.adapter.uploadList + +enum class UploadListType { CURRENT, COMPLETED, FAILED, CANCELLED, SKIPPED } diff --git a/app/src/test/java/com/owncloud/android/ui/db/OCUploadComparatorTest.kt b/app/src/test/java/com/owncloud/android/ui/db/OCUploadComparatorTest.kt deleted file mode 100644 index 6c18ffc3327f..000000000000 --- a/app/src/test/java/com/owncloud/android/ui/db/OCUploadComparatorTest.kt +++ /dev/null @@ -1,178 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2019 Daniele Fognini - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package com.owncloud.android.ui.db - -import com.owncloud.android.datamodel.UploadsStorageManager.UploadStatus.UPLOAD_FAILED -import com.owncloud.android.datamodel.UploadsStorageManager.UploadStatus.UPLOAD_IN_PROGRESS -import com.owncloud.android.db.OCUpload -import com.owncloud.android.db.OCUploadComparator -import org.junit.Assert.assertArrayEquals -import org.junit.Assert.assertEquals -import org.junit.BeforeClass -import org.junit.Test -import org.junit.runner.RunWith -import org.junit.runners.Parameterized -import org.junit.runners.Suite -import org.mockito.MockitoAnnotations -import org.mockito.kotlin.mock -import org.mockito.kotlin.whenever - -@RunWith(Suite::class) -@Suite.SuiteClasses( - OCUploadComparatorTest.Ordering::class, - OCUploadComparatorTest.ComparatorContract::class -) -class OCUploadComparatorTest { - - internal abstract class Base { - companion object { - val failed = mock(name = "failed") - val failedLater = mock(name = "failedLater") - val failedSameTimeOtherId = mock(name = "failedSameTimeOtherId") - val equalsNotSame = mock(name = "equalsNotSame") - val inProgress = mock(name = "InProgress") - val inProgressNow = mock(name = "inProgressNow") - private const val FIXED_UPLOAD_END_TIMESTAMP = 42L - private const val FIXED_UPLOAD_END_TIMESTAMP_LATER = 43L - private const val UPLOAD_ID = 40L - private const val UPLOAD_ID2 = 43L - - fun uploads(): Array = - arrayOf(failed, failedLater, inProgress, inProgressNow, failedSameTimeOtherId) - - @JvmStatic - @BeforeClass - fun setupMocks() { - MockitoAnnotations.initMocks(this) - - whenever(failed.fixedUploadStatus).thenReturn(UPLOAD_FAILED) - whenever(inProgress.fixedUploadStatus).thenReturn(UPLOAD_IN_PROGRESS) - whenever(inProgressNow.fixedUploadStatus).thenReturn(UPLOAD_IN_PROGRESS) - whenever(failedLater.fixedUploadStatus).thenReturn(UPLOAD_FAILED) - whenever(failedSameTimeOtherId.fixedUploadStatus).thenReturn(UPLOAD_FAILED) - whenever(equalsNotSame.fixedUploadStatus).thenReturn(UPLOAD_FAILED) - - whenever(inProgressNow.isFixedUploadingNow).thenReturn(true) - whenever(inProgress.isFixedUploadingNow).thenReturn(false) - - whenever(failed.fixedUploadEndTimeStamp).thenReturn(FIXED_UPLOAD_END_TIMESTAMP) - whenever(failedLater.fixedUploadEndTimeStamp).thenReturn(FIXED_UPLOAD_END_TIMESTAMP_LATER) - whenever(failedSameTimeOtherId.fixedUploadEndTimeStamp).thenReturn(FIXED_UPLOAD_END_TIMESTAMP) - whenever(equalsNotSame.fixedUploadEndTimeStamp).thenReturn(FIXED_UPLOAD_END_TIMESTAMP) - - whenever(failedLater.uploadId).thenReturn(UPLOAD_ID2) - whenever(failedSameTimeOtherId.uploadId).thenReturn(UPLOAD_ID) - whenever(equalsNotSame.uploadId).thenReturn(UPLOAD_ID) - } - } - } - - internal class Ordering : Base() { - - @Test - fun `same are compared equals in the list`() { - assertEquals(0, OCUploadComparator().compare(failed, failed)) - } - - @Test - fun `in progress is before failed in the list`() { - assertEquals(1, OCUploadComparator().compare(failed, inProgress)) - } - - @Test - fun `in progress uploading now is before in progress in the list`() { - assertEquals(1, OCUploadComparator().compare(inProgress, inProgressNow)) - } - - @Test - fun `later upload end is earlier in the list`() { - assertEquals(1, OCUploadComparator().compare(failed, failedLater)) - } - - @Test - fun `smaller upload id is earlier in the list`() { - assertEquals(1, OCUploadComparator().compare(failed, failedLater)) - } - - @Test - fun `same parameters compare equal in the list`() { - assertEquals(0, OCUploadComparator().compare(failedSameTimeOtherId, equalsNotSame)) - } - - @Test - fun `sort some uploads in the list`() { - val array = arrayOf( - inProgress, - inProgressNow, - failedSameTimeOtherId, - inProgressNow, - null, - failedLater, - failed - ) - - array.sortWith(OCUploadComparator()) - - assertArrayEquals( - arrayOf( - null, - inProgressNow, - inProgressNow, - inProgress, - failedLater, - failedSameTimeOtherId, - failed - ), - array - ) - } - } - - @RunWith(Parameterized::class) - internal class ComparatorContract( - private val upload1: OCUpload, - private val upload2: OCUpload, - private val upload3: OCUpload - ) : Base() { - companion object { - @JvmStatic - @Parameterized.Parameters(name = "{0}, {1}, {2}") - fun data(): List> = uploads().flatMap { u1 -> - uploads().flatMap { u2 -> - uploads().map { u3 -> - arrayOf(u1, u2, u3) - } - } - } - } - - @Test - fun `comparator is reflective`() { - assertEquals( - -OCUploadComparator().compare(upload1, upload2), - OCUploadComparator().compare(upload2, upload1) - ) - } - - @Test - fun `comparator is compatible with equals`() { - if (upload1 == upload2) { - assertEquals(0, OCUploadComparator().compare(upload1, upload2)) - } - } - - @Test - fun `comparator is transitive`() { - val compare12 = OCUploadComparator().compare(upload1, upload2) - val compare23 = OCUploadComparator().compare(upload2, upload3) - - if (compare12 == compare23) { - assertEquals(compare12, OCUploadComparator().compare(upload1, upload3)) - } - } - } -} diff --git a/app/src/test/java/com/owncloud/android/ui/db/OCUploadSortingTest.kt b/app/src/test/java/com/owncloud/android/ui/db/OCUploadSortingTest.kt new file mode 100644 index 000000000000..3e51f19bad3f --- /dev/null +++ b/app/src/test/java/com/owncloud/android/ui/db/OCUploadSortingTest.kt @@ -0,0 +1,120 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.owncloud.android.ui.db + +import com.nextcloud.utils.extensions.sortedByUploadOrder +import com.owncloud.android.datamodel.UploadsStorageManager.UploadStatus.UPLOAD_FAILED +import com.owncloud.android.datamodel.UploadsStorageManager.UploadStatus.UPLOAD_IN_PROGRESS +import com.owncloud.android.db.OCUpload +import org.junit.Assert.assertEquals +import org.junit.BeforeClass +import org.junit.Test +import org.mockito.MockitoAnnotations +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +class OCUploadSortingTest { + + companion object { + val failed = mock(name = "failed") + val failedLater = mock(name = "failedLater") + val failedSameTimeOtherId = mock(name = "failedSameTimeOtherId") + val equalsNotSame = mock(name = "equalsNotSame") + val inProgress = mock(name = "inProgress") + val inProgressNow = mock(name = "inProgressNow") + + private const val FIXED_UPLOAD_END_TIMESTAMP = 42L + private const val FIXED_UPLOAD_END_TIMESTAMP_LATER = 43L + private const val UPLOAD_ID = 40L + private const val UPLOAD_ID2 = 43L + + @JvmStatic + @BeforeClass + fun setupMocks() { + MockitoAnnotations.openMocks(this) + + whenever(failed.fixedUploadStatus).thenReturn(UPLOAD_FAILED) + whenever(inProgress.fixedUploadStatus).thenReturn(UPLOAD_IN_PROGRESS) + whenever(inProgressNow.fixedUploadStatus).thenReturn(UPLOAD_IN_PROGRESS) + whenever(failedLater.fixedUploadStatus).thenReturn(UPLOAD_FAILED) + whenever(failedSameTimeOtherId.fixedUploadStatus).thenReturn(UPLOAD_FAILED) + whenever(equalsNotSame.fixedUploadStatus).thenReturn(UPLOAD_FAILED) + + whenever(failed.fixedUploadId).thenReturn(UPLOAD_ID) + whenever(failedLater.fixedUploadId).thenReturn(UPLOAD_ID2) + whenever(failedSameTimeOtherId.fixedUploadId).thenReturn(UPLOAD_ID) + whenever(equalsNotSame.fixedUploadId).thenReturn(UPLOAD_ID) + + whenever(inProgressNow.isFixedUploadingNow).thenReturn(true) + whenever(inProgress.isFixedUploadingNow).thenReturn(false) + + whenever(failed.isFixedUploadingNow).thenReturn(false) + whenever(failedLater.isFixedUploadingNow).thenReturn(false) + whenever(failedSameTimeOtherId.isFixedUploadingNow).thenReturn(false) + whenever(equalsNotSame.isFixedUploadingNow).thenReturn(false) + + whenever(failed.fixedUploadEndTimeStamp).thenReturn(FIXED_UPLOAD_END_TIMESTAMP) + whenever(failedLater.fixedUploadEndTimeStamp).thenReturn(FIXED_UPLOAD_END_TIMESTAMP_LATER) + whenever(failedSameTimeOtherId.fixedUploadEndTimeStamp).thenReturn(FIXED_UPLOAD_END_TIMESTAMP) + whenever(equalsNotSame.fixedUploadEndTimeStamp).thenReturn(FIXED_UPLOAD_END_TIMESTAMP) + } + } + + @Test + fun `in progress comes before failed`() { + val result = listOf(failed, inProgress).sortedByUploadOrder() + assertEquals(listOf(inProgress, failed), result) + } + + @Test + fun `uploading now comes before not uploading`() { + val result = listOf(inProgress, inProgressNow).sortedByUploadOrder() + assertEquals(listOf(inProgressNow, inProgress), result) + } + + @Test + fun `later upload end comes first`() { + val result = listOf(failed, failedLater).sortedByUploadOrder() + assertEquals(listOf(failedLater, failed), result) + } + + @Test + fun `smaller upload id comes later when others equal`() { + val result = listOf(failedLater, failedSameTimeOtherId).sortedByUploadOrder() + assertEquals(listOf(failedLater, failedSameTimeOtherId), result) + } + + @Test + fun `same parameters keep stable ordering`() { + val result = listOf(failedSameTimeOtherId, equalsNotSame).sortedByUploadOrder() + assertEquals(listOf(failedSameTimeOtherId, equalsNotSame), result) + } + + @Test + fun `sort full list`() { + val result = listOf( + inProgress, + inProgressNow, + failedSameTimeOtherId, + inProgressNow, + failedLater, + failed + ).sortedByUploadOrder() + + assertEquals( + listOf( + inProgressNow, + inProgressNow, + inProgress, + failedLater, + failedSameTimeOtherId, + failed + ), + result + ) + } +}