Explorar el Código

Merge pull request #12549 from nextcloud/feature/resume_canceled_uploads

Add resume option for cancelled uploads
Alper Öztürk hace 1 año
padre
commit
63943bf5a4

+ 50 - 2
app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadHelper.kt

@@ -81,7 +81,6 @@ class FileUploadHelper {
         }
     }
 
-    @Suppress("ComplexCondition")
     fun retryFailedUploads(
         uploadsStorageManager: UploadsStorageManager,
         connectivityService: ConnectivityService,
@@ -92,7 +91,44 @@ class FileUploadHelper {
         if (failedUploads == null || failedUploads.isEmpty()) {
             return
         }
+        retryUploads(
+            uploadsStorageManager,
+            connectivityService,
+            accountManager,
+            powerManagementService,
+            failedUploads
+        )
+    }
+
+    fun retryCancelledUploads(
+        uploadsStorageManager: UploadsStorageManager,
+        connectivityService: ConnectivityService,
+        accountManager: UserAccountManager,
+        powerManagementService: PowerManagementService
+    ): Boolean {
+        val cancelledUploads = uploadsStorageManager.cancelledUploadsForCurrentAccount
+        if (cancelledUploads == null || cancelledUploads.isEmpty()) {
+            return false
+        }
+
+        return retryUploads(
+            uploadsStorageManager,
+            connectivityService,
+            accountManager,
+            powerManagementService,
+            cancelledUploads
+        )
+    }
 
+    @Suppress("ComplexCondition")
+    private fun retryUploads(
+        uploadsStorageManager: UploadsStorageManager,
+        connectivityService: ConnectivityService,
+        accountManager: UserAccountManager,
+        powerManagementService: PowerManagementService,
+        failedUploads: Array<OCUpload>
+    ): Boolean {
+        var showNotExistMessage = false
         val (gotNetwork, _, gotWifi) = connectivityService.connectivity
         val batteryStatus = powerManagementService.battery
         val charging = batteryStatus.isCharging || batteryStatus.isFull
@@ -105,6 +141,8 @@ class FileUploadHelper {
             }
             val isDeleted = !File(failedUpload.localPath).exists()
             if (isDeleted) {
+                showNotExistMessage = true
+
                 // 2A. for deleted files, mark as permanently failed
                 if (failedUpload.lastResult != UploadResult.FILE_NOT_FOUND) {
                     failedUpload.lastResult = UploadResult.FILE_NOT_FOUND
@@ -117,6 +155,8 @@ class FileUploadHelper {
                 retryUpload(failedUpload, uploadUser.get())
             }
         }
+
+        return showNotExistMessage
     }
 
     @Suppress("LongParameterList")
@@ -146,7 +186,7 @@ class FileUploadHelper {
         backgroundJobManager.startFilesUploadJob(user)
     }
 
-    fun cancelFileUpload(remotePath: String, accountName: String) {
+    fun removeFileUpload(remotePath: String, accountName: String) {
         try {
             val user = accountManager.getUser(accountName).get()
 
@@ -160,6 +200,14 @@ class FileUploadHelper {
         }
     }
 
+    fun cancelFileUpload(remotePath: String, accountName: String) {
+        uploadsStorageManager.getUploadByRemotePath(remotePath).run {
+            removeFileUpload(remotePath, accountName)
+            uploadStatus = UploadStatus.UPLOAD_CANCELLED
+            uploadsStorageManager.storeUpload(this)
+        }
+    }
+
     fun cancelAndRestartUploadJob(user: User) {
         backgroundJobManager.run {
             cancelFilesUploadJob(user)

+ 2 - 0
app/src/main/java/com/nextcloud/client/jobs/upload/FileUploadWorker.kt

@@ -143,6 +143,8 @@ class FileUploadWorker(
         val accountName = inputData.getString(ACCOUNT) ?: return Result.failure()
         var currentPage = uploadsStorageManager.getCurrentAndPendingUploadsForAccountPageAscById(-1, accountName)
 
+        notificationManager.dismissWorkerNotifications()
+
         while (currentPage.isNotEmpty() && !isStopped) {
             if (preferences.isGlobalUploadPaused) {
                 Log_OC.d(TAG, "Upload is paused, skip uploading files!")

+ 29 - 1
app/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java

@@ -630,6 +630,13 @@ public class UploadsStorageManager extends Observable {
                               ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", user.getAccountName());
     }
 
+    public OCUpload[] getCancelledUploadsForCurrentAccount() {
+        User user = currentAccountProvider.getUser();
+
+        return getUploads(ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_CANCELLED.value + AND +
+                              ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", user.getAccountName());
+    }
+
     /**
      * Get all uploads which where successfully completed.
      */
@@ -700,6 +707,20 @@ public class UploadsStorageManager extends Observable {
         return deleted;
     }
 
+    public void clearCancelledUploadsForCurrentAccount() {
+        User user = currentAccountProvider.getUser();
+        final long deleted = getDB().delete(
+            ProviderTableMeta.CONTENT_URI_UPLOADS,
+            ProviderTableMeta.UPLOADS_STATUS + "==" + UploadStatus.UPLOAD_CANCELLED.value + AND +
+                ProviderTableMeta.UPLOADS_ACCOUNT_NAME + "== ?", new String[]{user.getAccountName()}
+                                           );
+
+        Log_OC.d(TAG, "delete all cancelled uploads");
+        if (deleted > 0) {
+            notifyObserversNow();
+        }
+    }
+
     public long clearSuccessfulUploads() {
         User user = currentAccountProvider.getUser();
         final long deleted = getDB().delete(
@@ -851,7 +872,12 @@ public class UploadsStorageManager extends Observable {
         /**
          * Upload was successful.
          */
-        UPLOAD_SUCCEEDED(2);
+        UPLOAD_SUCCEEDED(2),
+
+        /**
+         * Upload was cancelled by the user.
+         */
+        UPLOAD_CANCELLED(3);
 
         private final int value;
 
@@ -867,6 +893,8 @@ public class UploadsStorageManager extends Observable {
                     return UPLOAD_FAILED;
                 case 2:
                     return UPLOAD_SUCCEEDED;
+                case 3:
+                    return UPLOAD_CANCELLED;
             }
             return null;
         }

+ 4 - 0
app/src/main/java/com/owncloud/android/db/OCUpload.java

@@ -390,6 +390,10 @@ public class OCUpload implements Parcelable {
         return this.useWifiOnly;
     }
 
+    public boolean exists() {
+        return new File(localPath).exists();
+    }
+
     public boolean isWhileChargingOnly() {
         return this.whileChargingOnly;
     }

+ 3 - 3
app/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.kt

@@ -94,7 +94,7 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
                 Decision.CANCEL -> {}
                 Decision.KEEP_LOCAL -> {
                     upload?.let {
-                        FileUploadHelper.instance().cancelFileUpload(it.remotePath, it.accountName)
+                        FileUploadHelper.instance().removeFileUpload(it.remotePath, it.accountName)
                     }
                     FileUploadHelper.instance().uploadUpdatedFile(
                         user,
@@ -106,7 +106,7 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
 
                 Decision.KEEP_BOTH -> {
                     upload?.let {
-                        FileUploadHelper.instance().cancelFileUpload(it.remotePath, it.accountName)
+                        FileUploadHelper.instance().removeFileUpload(it.remotePath, it.accountName)
                     }
                     FileUploadHelper.instance().uploadUpdatedFile(
                         user,
@@ -129,7 +129,7 @@ class ConflictsResolveActivity : FileActivity(), OnConflictDecisionMadeListener
                     }
 
                     upload?.let {
-                        FileUploadHelper.instance().cancelFileUpload(it.remotePath, it.accountName)
+                        FileUploadHelper.instance().removeFileUpload(it.remotePath, it.accountName)
 
                         UploadNotificationManager(
                             applicationContext,

+ 80 - 15
app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java

@@ -122,32 +122,34 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
 
         switch (group.type) {
             case CURRENT, FINISHED -> headerViewHolder.binding.uploadListAction.setImageResource(R.drawable.ic_close);
-            case FAILED -> headerViewHolder.binding.uploadListAction.setImageResource(R.drawable.ic_dots_vertical);
+            case CANCELLED, FAILED ->
+                headerViewHolder.binding.uploadListAction.setImageResource(R.drawable.ic_dots_vertical);
+
         }
 
         headerViewHolder.binding.uploadListAction.setOnClickListener(v -> {
             switch (group.type) {
                 case CURRENT -> {
-                    // cancel all current uploads
                     for (OCUpload upload : group.getItems()) {
                         uploadHelper.cancelFileUpload(upload.getRemotePath(), upload.getAccountName());
                     }
                     loadUploadItemsFromDb();
                 }
                 case FINISHED -> {
-                    // clear successfully uploaded section
                     uploadsStorageManager.clearSuccessfulUploads();
                     loadUploadItemsFromDb();
                 }
                 case FAILED -> {
-                    // show popup with option clear or retry filed uploads
-                    createFailedPopupMenu(headerViewHolder);
+                    showFailedPopupMenu(headerViewHolder);
+                }
+                case CANCELLED -> {
+                    showCancelledPopupMenu(headerViewHolder);
                 }
             }
         });
     }
 
-    private void createFailedPopupMenu(HeaderViewHolder headerViewHolder) {
+    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 -> {
@@ -173,6 +175,47 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
         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();
+            } else if (itemId == R.id.action_upload_list_cancelled_resume) {
+                retryCancelledUploads();
+            }
+
+            return true;
+        });
+
+        popup.show();
+    }
+
+    private void retryCancelledUploads() {
+        new Thread(() -> {
+            boolean showNotExistMessage = FileUploadHelper.Companion.instance().retryCancelledUploads(
+                uploadsStorageManager,
+                connectivityService,
+                accountManager,
+                powerManagementService);
+
+            parentActivity.runOnUiThread(this::loadUploadItemsFromDb);
+            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
@@ -197,7 +240,7 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
         this.clock = clock;
         this.viewThemeUtils = viewThemeUtils;
 
-        uploadGroups = new UploadGroup[3];
+        uploadGroups = new UploadGroup[4];
 
         shouldShowHeadersForEmptySections(false);
 
@@ -217,7 +260,16 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
             }
         };
 
-        uploadGroups[2] = new UploadGroup(Type.FINISHED,
+        uploadGroups[2] = new UploadGroup(Type.CANCELLED,
+                                          parentActivity.getString(
+                                              R.string.uploads_view_group_manually_cancelled_uploads)) {
+            @Override
+            public void refresh() {
+                fixAndSortItems(uploadsStorageManager.getCancelledUploadsForCurrentAccount());
+            }
+        };
+
+        uploadGroups[3] = new UploadGroup(Type.FINISHED,
                                           parentActivity.getString(R.string.uploads_view_group_finished_uploads)) {
             @Override
             public void refresh() {
@@ -324,11 +376,14 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
                 itemViewHolder.binding.uploadProgressBar.invalidate();
             }
             case UPLOAD_FAILED -> itemViewHolder.binding.uploadDate.setVisibility(View.GONE);
-            case UPLOAD_SUCCEEDED -> itemViewHolder.binding.uploadStatus.setVisibility(View.GONE);
+            case UPLOAD_SUCCEEDED, UPLOAD_CANCELLED ->
+                itemViewHolder.binding.uploadStatus.setVisibility(View.GONE);
         }
 
-        // show status if same file conflict or local file deleted
-        if (item.getUploadStatus() == UploadStatus.UPLOAD_SUCCEEDED && item.getLastResult() != UploadResult.UPLOADED) {
+        // 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.uploadDate.setVisibility(View.GONE);
             itemViewHolder.binding.uploadFileSize.setVisibility(View.GONE);
@@ -371,7 +426,9 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
         itemViewHolder.binding.thumbnail.setImageResource(R.drawable.file);
 
         // click on item
-        if (item.getUploadStatus() == UploadStatus.UPLOAD_FAILED) {
+        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) {
@@ -655,8 +712,16 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
                     status = parentActivity.getString(R.string.uploads_view_upload_status_succeeded);
                 }
             }
-            case UPLOAD_FAILED -> status = getUploadFailedStatusText(upload.getLastResult());
-            default -> status = "Uncontrolled status: " + upload.getUploadStatus();
+            case UPLOAD_FAILED -> {
+                status = getUploadFailedStatusText(upload.getLastResult());
+            }
+            case UPLOAD_CANCELLED -> {
+                status = parentActivity.getString(R.string.upload_manually_cancelled);
+            }
+
+            default -> {
+                status = "Uncontrolled status: " + upload.getUploadStatus();
+            }
         }
         return status;
     }
@@ -857,7 +922,7 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
     }
 
     enum Type {
-        CURRENT, FINISHED, FAILED
+        CURRENT, FINISHED, FAILED, CANCELLED
     }
 
     abstract class UploadGroup implements Refresh {

+ 32 - 0
app/src/main/res/menu/upload_list_cancelled_options.xml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Nextcloud Android client application
+
+ @author Jonas Mayer
+ Copyright (C) 2024 Jonas Mayer
+ Copyright (C) 2024 Nextcloud GmbH
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/action_upload_list_cancelled_resume"
+        android:title="@string/upload_action_cancelled_resume"
+        android:icon="@drawable/ic_sync" />
+
+    <item
+        android:id="@+id/action_upload_list_cancelled_clear"
+        android:title="@string/upload_action_cancelled_clear"
+        android:icon="@drawable/ic_close" />
+</menu>

+ 5 - 0
app/src/main/res/values/strings.xml

@@ -152,6 +152,7 @@
     <string name="uploads_view_title">Uploads</string>
     <string name="uploads_view_group_current_uploads">Current</string>
     <string name="uploads_view_group_failed_uploads">Failed/pending restart</string>
+    <string name="uploads_view_group_manually_cancelled_uploads">Cancelled</string>
     <string name="uploads_view_group_finished_uploads">Uploaded</string>
     <string name="uploads_view_upload_status_succeeded">Completed</string>
     <string name="uploads_view_upload_status_succeeded_same_file">Same file found on remote, skipping upload</string>
@@ -852,6 +853,10 @@
     <string name="upload_action_global_upload_pause">Pause all uploads</string>
     <string name="upload_action_global_upload_resume">Resume all uploads</string>
     <string name="dismiss_notification_description">Dismiss notification</string>
+    <string name="upload_action_file_not_exist_message">Some files not exists those files cannot be resumed</string>
+    <string name="upload_action_cancelled_resume">Resume cancelled uploads</string>
+    <string name="upload_action_cancelled_clear">Clear cancelled uploads</string>
+    <string name="upload_manually_cancelled">Upload was cancelled by user</string>
     <string name="action_empty_notifications">Clear all notifications</string>
     <string name="timeout_richDocuments">Loading is taking longer than expected</string>
     <string name="clear_notifications_failed">Failed to clear notifications.</string>