Browse Source

Merge pull request #12527 from nextcloud/feature/global_upload_pause

Add global upload pause button
Jonas Mayer 1 year ago
parent
commit
90f588ddad

+ 1 - 0
app/src/main/java/com/nextcloud/client/jobs/BackgroundJobFactory.kt

@@ -251,6 +251,7 @@ class BackgroundJobFactory @Inject constructor(
             viewThemeUtils.get(),
             localBroadcastManager.get(),
             backgroundJobManager.get(),
+            preferences,
             context,
             params
         )

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

@@ -160,7 +160,7 @@ class FileUploadHelper {
         }
     }
 
-    private fun cancelAndRestartUploadJob(user: User) {
+    fun cancelAndRestartUploadJob(user: User) {
         backgroundJobManager.run {
             cancelFilesUploadJob(user)
             startFilesUploadJob(user)

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

@@ -32,6 +32,7 @@ import com.nextcloud.client.device.PowerManagementService
 import com.nextcloud.client.jobs.BackgroundJobManager
 import com.nextcloud.client.jobs.BackgroundJobManagerImpl
 import com.nextcloud.client.network.ConnectivityService
+import com.nextcloud.client.preferences.AppPreferences
 import com.nextcloud.model.WorkerState
 import com.nextcloud.model.WorkerStateLiveData
 import com.owncloud.android.datamodel.FileDataStorageManager
@@ -58,6 +59,7 @@ class FileUploadWorker(
     val viewThemeUtils: ViewThemeUtils,
     val localBroadcastManager: LocalBroadcastManager,
     private val backgroundJobManager: BackgroundJobManager,
+    val preferences: AppPreferences,
     val context: Context,
     params: WorkerParameters
 ) : Worker(context, params), OnDatatransferProgressListener {
@@ -136,11 +138,20 @@ class FileUploadWorker(
         WorkerStateLiveData.instance().setWorkState(WorkerState.Idle)
     }
 
+    @Suppress("ReturnCount")
     private fun retrievePagesBySortingUploadsByID(): Result {
         val accountName = inputData.getString(ACCOUNT) ?: return Result.failure()
         var currentPage = uploadsStorageManager.getCurrentAndPendingUploadsForAccountPageAscById(-1, accountName)
 
         while (currentPage.isNotEmpty() && !isStopped) {
+            if (preferences.isGlobalUploadPaused) {
+                Log_OC.d(TAG, "Upload is paused, skip uploading files!")
+                notificationManager.notifyPaused(
+                    intents.notificationStartIntent(null)
+                )
+                return Result.success()
+            }
+
             Log_OC.d(TAG, "Handling ${currentPage.size} uploads for account $accountName")
             val lastId = currentPage.last().uploadId
             uploadFiles(currentPage, accountName)

+ 3 - 3
app/src/main/java/com/nextcloud/client/jobs/upload/FileUploaderIntents.kt

@@ -97,10 +97,10 @@ class FileUploaderIntents(private val context: Context) {
         )
     }
 
-    fun notificationStartIntent(operation: UploadFileOperation): PendingIntent {
+    fun notificationStartIntent(operation: UploadFileOperation?): PendingIntent {
         val intent = UploadListActivity.createIntent(
-            operation.file,
-            operation.user,
+            operation?.file,
+            operation?.user,
             Intent.FLAG_ACTIVITY_CLEAR_TOP,
             context
         )

+ 23 - 15
app/src/main/java/com/nextcloud/client/jobs/upload/UploadNotificationManager.kt

@@ -35,17 +35,14 @@ import com.owncloud.android.operations.UploadFileOperation
 import com.owncloud.android.ui.notifications.NotificationUtils
 import com.owncloud.android.utils.theme.ViewThemeUtils
 
-class UploadNotificationManager(private val context: Context, private val viewThemeUtils: ViewThemeUtils) {
+class UploadNotificationManager(private val context: Context, viewThemeUtils: ViewThemeUtils) {
     companion object {
         private const val ID = 411
     }
 
     private var notification: Notification? = null
-    private var notificationBuilder: NotificationCompat.Builder
-    private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
-
-    init {
-        notificationBuilder = NotificationUtils.newNotificationBuilder(context, viewThemeUtils).apply {
+    private var notificationBuilder: NotificationCompat.Builder =
+        NotificationUtils.newNotificationBuilder(context, viewThemeUtils).apply {
             setContentTitle(context.getString(R.string.foreground_service_upload))
             setSmallIcon(R.drawable.notification_icon)
             setLargeIcon(BitmapFactory.decodeResource(context.resources, R.drawable.notification_icon))
@@ -54,18 +51,16 @@ class UploadNotificationManager(private val context: Context, private val viewTh
                 setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_UPLOAD)
             }
         }
+    private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
 
+    init {
         notification = notificationBuilder.build()
     }
 
     @Suppress("MagicNumber")
     fun prepareForStart(upload: UploadFileOperation, pendingIntent: PendingIntent, startIntent: PendingIntent) {
-        notificationBuilder = NotificationUtils.newNotificationBuilder(context, viewThemeUtils).apply {
-            setSmallIcon(R.drawable.notification_icon)
-            setOngoing(true)
-            setTicker(context.getString(R.string.foreground_service_upload))
+        notificationBuilder.run {
             setContentTitle(context.getString(R.string.uploader_upload_in_progress_ticker))
-            setProgress(100, 0, false)
             setContentText(
                 String.format(
                     context.getString(R.string.uploader_upload_in_progress),
@@ -73,6 +68,9 @@ class UploadNotificationManager(private val context: Context, private val viewTh
                     upload.fileName
                 )
             )
+            setTicker(context.getString(R.string.foreground_service_upload))
+            setProgress(100, 0, false)
+            setOngoing(true)
             clearActions()
 
             addAction(
@@ -81,10 +79,6 @@ class UploadNotificationManager(private val context: Context, private val viewTh
                 pendingIntent
             )
 
-            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-                setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_UPLOAD)
-            }
-
             setContentIntent(startIntent)
         }
 
@@ -192,4 +186,18 @@ class UploadNotificationManager(private val context: Context, private val viewTh
     fun dismissWorkerNotifications() {
         notificationManager.cancel(ID)
     }
+
+    fun notifyPaused(intent: PendingIntent) {
+        notificationBuilder.apply {
+            setContentTitle(context.getString(R.string.upload_global_pause_title))
+            setTicker(context.getString(R.string.upload_global_pause_title))
+            setOngoing(true)
+            setAutoCancel(false)
+            setProgress(0, 0, false)
+            clearActions()
+            setContentIntent(intent)
+        }
+
+        showNotification()
+    }
 }

+ 4 - 0
app/src/main/java/com/nextcloud/client/preferences/AppPreferences.java

@@ -387,6 +387,10 @@ public interface AppPreferences {
 
     void setCalendarLastBackup(long timestamp);
 
+    boolean isGlobalUploadPaused();
+
+    void setGlobalUploadPaused(boolean globalPausedState);
+
     void setPdfZoomTipShownCount(int count);
 
     int getPdfZoomTipShownCount();

+ 12 - 0
app/src/main/java/com/nextcloud/client/preferences/AppPreferencesImpl.java

@@ -107,6 +107,8 @@ public final class AppPreferencesImpl implements AppPreferences {
     private static final String PREF__CALENDAR_AUTOMATIC_BACKUP = "calendar_automatic_backup";
     private static final String PREF__CALENDAR_LAST_BACKUP = "calendar_last_backup";
 
+    private static final String PREF__GLOBAL_PAUSE_STATE = "global_pause_state";
+
     private static final String PREF__PDF_ZOOM_TIP_SHOWN = "pdf_zoom_tip_shown";
     private static final String PREF__MEDIA_FOLDER_LAST_PATH = "media_folder_last_path";
 
@@ -741,6 +743,16 @@ public final class AppPreferencesImpl implements AppPreferences {
         preferences.edit().putLong(PREF__CALENDAR_LAST_BACKUP, timestamp).apply();
     }
 
+    @Override
+    public boolean isGlobalUploadPaused() {
+        return preferences.getBoolean(PREF__GLOBAL_PAUSE_STATE,false);
+    }
+
+    @Override
+    public void setGlobalUploadPaused(boolean globalPausedState) {
+        preferences.edit().putBoolean(PREF__GLOBAL_PAUSE_STATE, globalPausedState).apply();
+    }
+
     @Override
     public void setPdfZoomTipShownCount(int count) {
         preferences.edit().putInt(PREF__PDF_ZOOM_TIP_SHOWN, count).apply();

+ 4 - 0
app/src/main/java/com/owncloud/android/ui/activity/DrawerActivity.java

@@ -1155,6 +1155,10 @@ public abstract class DrawerActivity extends ToolbarActivity
         return true;
     }
 
+    public AppPreferences getAppPreferences(){
+        return preferences;
+    }
+
     @Override
     protected void onStart() {
         super.onStart();

+ 55 - 21
app/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java

@@ -24,6 +24,7 @@
 package com.owncloud.android.ui.activity;
 
 import android.accounts.Account;
+import android.annotation.SuppressLint;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -49,7 +50,6 @@ import com.owncloud.android.R;
 import com.owncloud.android.databinding.UploadListLayoutBinding;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.UploadsStorageManager;
-import com.owncloud.android.db.OCUpload;
 import com.owncloud.android.lib.common.operations.RemoteOperation;
 import com.owncloud.android.lib.common.operations.RemoteOperationResult;
 import com.owncloud.android.lib.common.utils.Log_OC;
@@ -65,11 +65,11 @@ import javax.inject.Inject;
 import androidx.localbroadcastmanager.content.LocalBroadcastManager;
 import androidx.recyclerview.widget.GridLayoutManager;
 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
 
 /**
- * Activity listing pending, active, and completed uploads. User can delete
- * completed uploads from view. Content of this list of coming from
- * {@link UploadsStorageManager}.
+ * Activity listing pending, active, and completed uploads. User can delete completed uploads from view. Content of this
+ * list of coming from {@link UploadsStorageManager}.
  */
 public class UploadListActivity extends FileActivity {
 
@@ -210,13 +210,17 @@ public class UploadListActivity extends FileActivity {
     private void refresh() {
         backgroundJobManager.startImmediateFilesSyncJob(false, true);
 
-        if(uploadsStorageManager.getFailedUploads().length > 0){
-            new Thread(() -> FileUploadHelper.Companion.instance().retryFailedUploads(
-                uploadsStorageManager,
-                connectivityService,
-                userAccountManager,
-                powerManagementService))
-                .start();
+        if (uploadsStorageManager.getFailedUploads().length > 0) {
+            new Thread(() -> {
+                FileUploadHelper.Companion.instance().retryFailedUploads(
+                    uploadsStorageManager,
+                    connectivityService,
+                    accountManager,
+                    powerManagementService);
+                this.runOnUiThread(() -> {
+                    uploadListAdapter.loadUploadItemsFromDb();
+                });
+            }).start();
             DisplayUtils.showSnackMessage(this, R.string.uploader_local_files_uploaded);
         }
 
@@ -265,13 +269,47 @@ public class UploadListActivity extends FileActivity {
     public boolean onCreateOptionsMenu(Menu menu) {
         MenuInflater inflater = getMenuInflater();
         inflater.inflate(R.menu.activity_upload_list, menu);
-
+        updateGlobalPauseIcon(menu.getItem(0));
         return true;
     }
 
+    @SuppressFBWarnings("RV_RETURN_VALUE_IGNORED_NO_SIDE_EFFECT")
+    private void updateGlobalPauseIcon(MenuItem pauseMenuItem) {
+        if (pauseMenuItem.getItemId() != R.id.action_toggle_global_pause) {
+            return;
+        }
+
+        int iconId;
+        String title;
+        if (preferences.isGlobalUploadPaused()) {
+            iconId = R.drawable.ic_play;
+            title = getString(R.string.upload_action_global_upload_resume);
+        } else {
+            iconId = R.drawable.ic_pause;
+            title = getString(R.string.upload_action_global_upload_pause);
+        }
+
+        pauseMenuItem.setIcon(iconId);
+        pauseMenuItem.setTitle(title);
+    }
+
+    @SuppressLint("NotifyDataSetChanged")
+    private void toggleGlobalPause(MenuItem pauseMenuItem) {
+        preferences.setGlobalUploadPaused(!preferences.isGlobalUploadPaused());
+        updateGlobalPauseIcon(pauseMenuItem);
+
+        for (User user : accountManager.getAllUsers()) {
+            if (user != null) {
+                FileUploadHelper.Companion.instance().cancelAndRestartUploadJob(user);
+            }
+        }
+
+        uploadListAdapter.notifyDataSetChanged();
+    }
+
     @Override
     public boolean onOptionsItemSelected(MenuItem item) {
-        boolean retval = true;
+
         int itemId = item.getItemId();
 
         if (itemId == android.R.id.home) {
@@ -280,17 +318,13 @@ public class UploadListActivity extends FileActivity {
             } else {
                 openDrawer();
             }
-        } else if (itemId == R.id.action_clear_failed_uploads) {
-            for (OCUpload upload : uploadsStorageManager.getFailedButNotDelayedUploadsForCurrentAccount()){
-                uploadListAdapter.cancelOldErrorNotification(upload);
-            }
-            uploadsStorageManager.clearFailedButNotDelayedUploads();
-            uploadListAdapter.loadUploadItemsFromDb();
+        } else if (itemId == R.id.action_toggle_global_pause) {
+            toggleGlobalPause(item);
         } else {
-            retval = super.onOptionsItemSelected(item);
+            return super.onOptionsItemSelected(item);
         }
 
-        return retval;
+        return true;
     }
 
     @Override

+ 85 - 64
app/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java

@@ -29,7 +29,6 @@ import android.content.ActivityNotFoundException;
 import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
-import android.graphics.Typeface;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.text.format.DateUtils;
@@ -123,29 +122,55 @@ 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_sync);
+            case 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 FINISHED -> uploadsStorageManager.clearSuccessfulUploads();
-                case FAILED -> new Thread(() -> FileUploadHelper.Companion.instance().retryFailedUploads(
-                    uploadsStorageManager,
-                    connectivityService,
-                    accountManager,
-                    powerManagementService)).start();
-                default -> {
+                case FAILED -> {
+                    // show popup with option clear or retry filed uploads
+                    createFailedPopupMenu(headerViewHolder);
                 }
-                // do nothing
             }
+        });
+    }
+
+    private void createFailedPopupMenu(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();
+                loadUploadItemsFromDb();
+            } else {
 
-            loadUploadItemsFromDb();
+                new Thread(() -> {
+                    FileUploadHelper.Companion.instance().retryFailedUploads(
+                        uploadsStorageManager,
+                        connectivityService,
+                        accountManager,
+                        powerManagementService);
+                    parentActivity.runOnUiThread(this::loadUploadItemsFromDb);
+                }).start();
+
+            }
+            return true;
         });
+        failedPopup.show();
     }
 
     @Override
@@ -228,7 +253,7 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
         // file size
         if (item.getFileSize() != 0) {
             itemViewHolder.binding.uploadFileSize.setText(String.format("%s, ",
-                    DisplayUtils.bytesToHumanReadable(item.getFileSize())));
+                                                                        DisplayUtils.bytesToHumanReadable(item.getFileSize())));
         } else {
             itemViewHolder.binding.uploadFileSize.setText("");
         }
@@ -260,7 +285,6 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
         itemViewHolder.binding.uploadRemotePath.setVisibility(View.VISIBLE);
         itemViewHolder.binding.uploadFileSize.setVisibility(View.VISIBLE);
         itemViewHolder.binding.uploadStatus.setVisibility(View.VISIBLE);
-        itemViewHolder.binding.uploadStatus.setTypeface(null, Typeface.NORMAL);
         itemViewHolder.binding.uploadProgressBar.setVisibility(View.GONE);
 
         // Update information depending of upload details
@@ -304,9 +328,8 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
         }
 
         // show status if same file conflict or local file deleted
-        if (item.getUploadStatus() == UploadStatus.UPLOAD_SUCCEEDED && item.getLastResult() != UploadResult.UPLOADED){
+        if (item.getUploadStatus() == UploadStatus.UPLOAD_SUCCEEDED && item.getLastResult() != UploadResult.UPLOADED) {
             itemViewHolder.binding.uploadStatus.setVisibility(View.VISIBLE);
-            itemViewHolder.binding.uploadStatus.setTypeface(null, Typeface.BOLD);
             itemViewHolder.binding.uploadDate.setVisibility(View.GONE);
             itemViewHolder.binding.uploadFileSize.setVisibility(View.GONE);
         }
@@ -373,16 +396,16 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
                     DisplayUtils.showSnackMessage(
                         v.getRootView().findViewById(android.R.id.content),
                         R.string.local_file_not_found_message
-                    );
+                                                 );
                 }
             });
-        } else if (item.getUploadStatus() == UploadStatus.UPLOAD_SUCCEEDED){
+        } 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){
+        if (item.getUploadStatus() != UploadStatus.UPLOAD_SUCCEEDED) {
             itemViewHolder.binding.thumbnail.setOnClickListener(v -> onUploadingItemClick(item));
         }
 
@@ -395,17 +418,17 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
         fakeFileToCheatThumbnailsCacheManagerInterface.setMimeType(item.getMimeType());
 
         boolean allowedToCreateNewThumbnail = ThumbnailsCacheManager.cancelPotentialThumbnailWork(
-                fakeFileToCheatThumbnailsCacheManagerInterface, itemViewHolder.binding.thumbnail
-        );
+            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) {
+            && fakeFileToCheatThumbnailsCacheManagerInterface.getRemoteId() != null &&
+            item.getUploadStatus() == UploadStatus.UPLOAD_SUCCEEDED) {
             // Thumbnail in Cache?
             Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(
-                    String.valueOf(fakeFileToCheatThumbnailsCacheManagerInterface.getRemoteId())
-            );
+                String.valueOf(fakeFileToCheatThumbnailsCacheManagerInterface.getRemoteId())
+                                                                            );
             if (thumbnail != null && !fakeFileToCheatThumbnailsCacheManagerInterface.isUpdateThumbnailNeeded()) {
                 itemViewHolder.binding.thumbnail.setImageBitmap(thumbnail);
             } else {
@@ -413,11 +436,11 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
                 Optional<User> user = parentActivity.getUser();
                 if (allowedToCreateNewThumbnail && user.isPresent()) {
                     final ThumbnailsCacheManager.ThumbnailGenerationTask task =
-                            new ThumbnailsCacheManager.ThumbnailGenerationTask(
-                                itemViewHolder.binding.thumbnail,
-                                parentActivity.getStorageManager(),
-                                user.get()
-                            );
+                        new ThumbnailsCacheManager.ThumbnailGenerationTask(
+                            itemViewHolder.binding.thumbnail,
+                            parentActivity.getStorageManager(),
+                            user.get()
+                        );
                     if (thumbnail == null) {
                         if (MimeTypeUtil.isVideo(fakeFileToCheatThumbnailsCacheManagerInterface)) {
                             thumbnail = ThumbnailsCacheManager.mDefaultVideo;
@@ -426,20 +449,20 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
                         }
                     }
                     final ThumbnailsCacheManager.AsyncThumbnailDrawable asyncDrawable =
-                            new ThumbnailsCacheManager.AsyncThumbnailDrawable(
-                                parentActivity.getResources(),
-                                thumbnail,
-                                task
-                            );
+                        new ThumbnailsCacheManager.AsyncThumbnailDrawable(
+                            parentActivity.getResources(),
+                            thumbnail,
+                            task
+                        );
                     itemViewHolder.binding.thumbnail.setImageDrawable(asyncDrawable);
                     task.execute(new ThumbnailsCacheManager.ThumbnailGenerationTaskObject(
-                            fakeFileToCheatThumbnailsCacheManagerInterface, null));
+                        fakeFileToCheatThumbnailsCacheManagerInterface, null));
                 }
             }
 
             if ("image/png".equals(item.getMimeType())) {
                 itemViewHolder.binding.thumbnail.setBackgroundColor(parentActivity.getResources()
-                        .getColor(R.color.bg_default));
+                                                                        .getColor(R.color.bg_default));
             }
 
 
@@ -447,14 +470,14 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
             File file = new File(item.getLocalPath());
             // Thumbnail in Cache?
             Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(
-                    String.valueOf(file.hashCode()));
+                String.valueOf(file.hashCode()));
             if (thumbnail != null) {
                 itemViewHolder.binding.thumbnail.setImageBitmap(thumbnail);
             } else {
                 // generate new Thumbnail
                 if (allowedToCreateNewThumbnail) {
                     final ThumbnailsCacheManager.ThumbnailGenerationTask task =
-                            new ThumbnailsCacheManager.ThumbnailGenerationTask(itemViewHolder.binding.thumbnail);
+                        new ThumbnailsCacheManager.ThumbnailGenerationTask(itemViewHolder.binding.thumbnail);
 
                     if (MimeTypeUtil.isVideo(file)) {
                         thumbnail = ThumbnailsCacheManager.mDefaultVideo;
@@ -474,7 +497,7 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
 
             if ("image/png".equalsIgnoreCase(item.getMimeType())) {
                 itemViewHolder.binding.thumbnail.setBackgroundColor(parentActivity.getResources()
-                        .getColor(R.color.bg_default));
+                                                                        .getColor(R.color.bg_default));
             }
         } else {
             if (optionalUser.isPresent()) {
@@ -605,8 +628,7 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
     }
 
     /**
-     * Gets the status text to show to the user according to the status and last result of the
-     * the given upload.
+     * 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.
@@ -614,30 +636,27 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
     private String getStatusText(OCUpload upload) {
         String status;
         switch (upload.getUploadStatus()) {
-            case UPLOAD_IN_PROGRESS:
+            case UPLOAD_IN_PROGRESS -> {
                 status = parentActivity.getString(R.string.uploads_view_later_waiting_to_upload);
                 if (uploadHelper.isUploadingNow(upload)) {
                     // really uploading, bind the progress bar to listen for progress updates
                     status = parentActivity.getString(R.string.uploader_upload_in_progress_ticker);
                 }
-                break;
-
-            case UPLOAD_SUCCEEDED:
-                if (upload.getLastResult() == UploadResult.SAME_FILE_CONFLICT){
+                if (parentActivity.getAppPreferences().isGlobalUploadPaused()) {
+                    status = parentActivity.getString(R.string.upload_global_pause_title);
+                }
+            }
+            case UPLOAD_SUCCEEDED -> {
+                if (upload.getLastResult() == UploadResult.SAME_FILE_CONFLICT) {
                     status = parentActivity.getString(R.string.uploads_view_upload_status_succeeded_same_file);
-                }else if (upload.getLastResult() == UploadResult.FILE_NOT_FOUND) {
+                } else if (upload.getLastResult() == UploadResult.FILE_NOT_FOUND) {
                     status = getUploadFailedStatusText(upload.getLastResult());
                 } else {
                     status = parentActivity.getString(R.string.uploads_view_upload_status_succeeded);
                 }
-                break;
-
-            case UPLOAD_FAILED:
-                status = getUploadFailedStatusText(upload.getLastResult());
-                break;
-
-            default:
-                status = "Uncontrolled status: " + upload.getUploadStatus();
+            }
+            case UPLOAD_FAILED -> status = getUploadFailedStatusText(upload.getLastResult());
+            default -> status = "Uncontrolled status: " + upload.getUploadStatus();
         }
         return status;
     }
@@ -690,8 +709,8 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
             case SSL_RECOVERABLE_PEER_UNVERIFIED:
                 status =
                     parentActivity.getString(
-                                R.string.uploads_view_upload_status_failed_ssl_certificate_not_trusted
-                        );
+                        R.string.uploads_view_upload_status_failed_ssl_certificate_not_trusted
+                                            );
                 break;
             case UNKNOWN:
                 status = parentActivity.getString(R.string.uploads_view_upload_status_unknown_fail);
@@ -701,7 +720,7 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
                 break;
             case DELAYED_IN_POWER_SAVE_MODE:
                 status = parentActivity.getString(
-                        R.string.uploads_view_upload_status_waiting_exit_power_save_mode);
+                    R.string.uploads_view_upload_status_waiting_exit_power_save_mode);
                 break;
             case VIRUS_DETECTED:
                 status = parentActivity.getString(R.string.uploads_view_upload_status_virus_detected);
@@ -776,17 +795,17 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
      */
     private void onUploadedItemClick(OCUpload upload) {
         final OCFile file = parentActivity.getStorageManager().getFileByEncryptedRemotePath(upload.getRemotePath());
-        if (file == null){
+        if (file == null) {
             DisplayUtils.showSnackMessage(parentActivity, R.string.error_retrieving_file);
             Log_OC.i(TAG, "Could not find uploaded file on remote.");
             return;
         }
 
-        if (PreviewImageFragment.canBePreviewed(file)){
+        if (PreviewImageFragment.canBePreviewed(file)) {
             //show image preview and stay in uploads tab
             Intent intent = FileDisplayActivity.openFileIntent(parentActivity, parentActivity.getUser().get(), file);
             parentActivity.startActivity(intent);
-        }else{
+        } else {
             Intent intent = new Intent(parentActivity, FileDisplayActivity.class);
             intent.setAction(Intent.ACTION_VIEW);
             intent.putExtra(FileDisplayActivity.KEY_FILE_PATH, upload.getRemotePath());
@@ -882,14 +901,16 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
         }
     }
 
-    public void cancelOldErrorNotification(OCUpload upload){
+    public void cancelOldErrorNotification(OCUpload upload) {
 
         if (mNotificationManager == null) {
             mNotificationManager = (NotificationManager) parentActivity.getSystemService(parentActivity.NOTIFICATION_SERVICE);
         }
 
-        if (upload == null) return;
-        mNotificationManager.cancel(NotificationUtils.createUploadNotificationTag(upload.getRemotePath(),upload.getLocalPath()),
+        if (upload == null) {
+            return;
+        }
+        mNotificationManager.cancel(NotificationUtils.createUploadNotificationTag(upload.getRemotePath(), upload.getLocalPath()),
                                     FileUploadWorker.NOTIFICATION_ERROR_ID);
 
     }

+ 9 - 0
app/src/main/res/drawable/ic_pause.xml

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960">
+  <path
+      android:fillColor="@color/foreground_highlight"
+      android:pathData="M560,760v-560h160v560L560,760ZM240,760v-560h160v560L240,760Z"/>
+</vector>

+ 30 - 0
app/src/main/res/drawable/ic_play.xml

@@ -0,0 +1,30 @@
+<!--
+  ~ Nextcloud Android client application
+  ~
+  ~ @author Alper Ozturk
+  ~ Copyright (C) 2023 Alper Ozturk
+  ~ Copyright (C) 2023 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/>.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="24dp"
+    android:height="24dp"
+    android:viewportWidth="960"
+    android:viewportHeight="960">
+  <path
+      android:fillColor="@color/foreground_highlight"
+      android:pathData="M320,760v-560l440,280 -440,280Z"/>
+</vector>

+ 6 - 3
app/src/main/res/menu/activity_upload_list.xml

@@ -16,13 +16,16 @@
   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 -->
-<menu xmlns:android="http://schemas.android.com/apk/res/android">
+<menu xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:android="http://schemas.android.com/apk/res/android">
     <group
         android:id="@+id/upload_list_actions"
         android:checkableBehavior="none">
 
         <item
-            android:id="@+id/action_clear_failed_uploads"
-            android:title="@string/action_clear_failed_uploads" />
+            android:id="@+id/action_toggle_global_pause"
+            android:icon="@android:drawable/ic_media_pause"
+            android:title="@string/upload_action_global_upload_pause"
+            app:showAsAction="always" />
     </group>
 </menu>

+ 32 - 0
app/src/main/res/menu/upload_list_failed_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_failed_retry"
+        android:icon="@drawable/ic_sync"
+        android:title="@string/upload_action_failed_retry" />
+
+    <item
+        android:id="@+id/action_upload_list_failed_clear"
+        android:title="@string/upload_action_failed_clear"
+        android:icon="@drawable/ic_close" />
+</menu>

+ 1 - 0
app/src/main/res/values-night/colors.xml

@@ -33,6 +33,7 @@
     <color name="grey_200">#818181</color>
     <color name="nc_grey">#222222</color>
     <color name="icon_on_nc_grey">#ffffff</color>
+    <color name="foreground_highlight">#EAE0E5</color>
 
     <!-- Multiselect backgrounds -->
     <color name="action_mode_background">@color/appbar</color>

+ 1 - 0
app/src/main/res/values/colors.xml

@@ -59,6 +59,7 @@
     <color name="secondary_button_text_color">#000000</color>
     <color name="nc_grey">#ededed</color>
     <color name="icon_on_nc_grey">#000000</color>
+    <color name="foreground_highlight">#1D1B1E</color>
 
     <color name="process_dialog_background">#ffffff</color>
     <color name="indicator_dot_selected">#ffffff</color>

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

@@ -522,8 +522,6 @@
     <string name="share_room_clarification">%1$s (conversation)</string>
     <string name="share_known_remote_on_clarification">on %1$s</string>
 
-    <string name="action_clear_failed_uploads">Clear failed uploads</string>
-
     <string name="action_switch_grid_view">Grid view</string>
     <string name="action_switch_list_view">List view</string>
 
@@ -845,9 +843,14 @@
     <string name="upload_sync_conflict">Sync conflict, please resolve manually</string>
     <string name="upload_cannot_create_file">Cannot create local file</string>
     <string name="upload_local_storage_not_copied">File could not be copied to local storage</string>
+    <string name="upload_global_pause_title">All uploads are paused</string>
     <string name="upload_quota_exceeded">Storage quota exceeded</string>
     <string name="host_not_available">Server not available</string>
     <string name="delete_entries">Delete entries</string>
+    <string name="upload_action_failed_retry">Retry failed uploads</string>
+    <string name="upload_action_failed_clear">Clear failed uploads</string>
+    <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="action_empty_notifications">Clear all notifications</string>
     <string name="timeout_richDocuments">Loading is taking longer than expected</string>