Pārlūkot izejas kodu

Merge pull request #4788 from nextcloud/instantupload_all

Setting to also upload existing files
Tobias Kaminsky 5 gadi atpakaļ
vecāks
revīzija
5d428fe9a2
34 mainītis faili ar 1010 papildinājumiem un 1186 dzēšanām
  1. 1 1
      scripts/analysis/findbugs-results.txt
  2. 12 12
      src/androidTest/java/com/owncloud/android/UploadIT.java
  3. 1 1
      src/main/java/com/owncloud/android/MainApp.java
  4. 7 2
      src/main/java/com/owncloud/android/datamodel/SyncedFolder.java
  5. 7 4
      src/main/java/com/owncloud/android/datamodel/SyncedFolderDisplayItem.java
  6. 6 2
      src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java
  7. 3 3
      src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java
  8. 5 5
      src/main/java/com/owncloud/android/db/OCUpload.java
  9. 3 2
      src/main/java/com/owncloud/android/db/ProviderMeta.java
  10. 11 0
      src/main/java/com/owncloud/android/files/services/FileDownloader.java
  11. 264 763
      src/main/java/com/owncloud/android/files/services/FileUploader.java
  12. 12 12
      src/main/java/com/owncloud/android/jobs/ContactsBackupJob.java
  13. 23 19
      src/main/java/com/owncloud/android/jobs/FilesSyncJob.java
  14. 7 2
      src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java
  15. 57 44
      src/main/java/com/owncloud/android/operations/UploadFileOperation.java
  16. 65 1
      src/main/java/com/owncloud/android/providers/FileContentProvider.java
  17. 81 34
      src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.java
  18. 3 3
      src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java
  19. 5 5
      src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java
  20. 18 10
      src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.java
  21. 16 9
      src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java
  22. 147 39
      src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java
  23. 3 3
      src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java
  24. 36 32
      src/main/java/com/owncloud/android/ui/dialog/ConflictsResolveDialog.java
  25. 17 0
      src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.java
  26. 4 0
      src/main/java/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelable.java
  27. 12 12
      src/main/java/com/owncloud/android/ui/helpers/UriUploader.java
  28. 9 7
      src/main/java/com/owncloud/android/utils/ErrorMessageAdapter.java
  29. 25 16
      src/main/java/com/owncloud/android/utils/FilesSyncHelper.java
  30. 112 143
      src/main/res/layout/synced_folders_settings_layout.xml
  31. 30 0
      src/main/res/menu/upload_list_item_file_conflict.xml
  32. 1 0
      src/main/res/values/dims.xml
  33. 6 0
      src/main/res/values/strings.xml
  34. 1 0
      src/test/java/com/owncloud/android/ui/activity/SyncedFoldersActivityTest.java

+ 1 - 1
scripts/analysis/findbugs-results.txt

@@ -1 +1 @@
-385
+383

+ 12 - 12
src/androidTest/java/com/owncloud/android/UploadIT.java

@@ -120,7 +120,7 @@ public class UploadIT extends AbstractIT {
             account,
             null,
             ocUpload,
-            false,
+            FileUploader.NameCollisionPolicy.DEFAULT,
             FileUploader.LOCAL_BEHAVIOUR_COPY,
             targetContext,
             false,
@@ -140,17 +140,17 @@ public class UploadIT extends AbstractIT {
         OCUpload ocUpload = new OCUpload(FileStorageUtils.getSavePath(account.name) + "/empty.txt",
                 "/testUpload/2/3/4/1.txt", account.name);
         UploadFileOperation newUpload = new UploadFileOperation(
-                storageManager,
-                connectivityServiceMock,
-                powerManagementServiceMock,
-                account,
-                null,
-                ocUpload,
-                false,
-                FileUploader.LOCAL_BEHAVIOUR_COPY,
-                targetContext,
-                false,
-                false
+            storageManager,
+            connectivityServiceMock,
+            powerManagementServiceMock,
+            account,
+            null,
+            ocUpload,
+            FileUploader.NameCollisionPolicy.DEFAULT,
+            FileUploader.LOCAL_BEHAVIOUR_COPY,
+            targetContext,
+            false,
+            false
         );
         newUpload.addRenameUploadListener(() -> {
             // dummy

+ 1 - 1
src/main/java/com/owncloud/android/MainApp.java

@@ -773,7 +773,7 @@ public class MainApp extends MultiDexApplication implements HasAndroidInjector {
 
                 for (SyncedFolder syncedFolder : syncedFolderProvider.getSyncedFolders()) {
                     if (syncedFolder.isEnabled()) {
-                        FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder);
+                        FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder, true);
                     }
                 }
 

+ 7 - 2
src/main/java/com/owncloud/android/datamodel/SyncedFolder.java

@@ -39,6 +39,7 @@ public class SyncedFolder implements Serializable, Cloneable {
     @Getter @Setter private String remotePath;
     @Getter @Setter private boolean wifiOnly;
     @Getter @Setter private boolean chargingOnly;
+    @Getter @Setter private boolean existing;
     @Getter @Setter private boolean subfolderByDate;
     @Getter @Setter private String account;
     @Getter @Setter private int uploadAction;
@@ -54,6 +55,7 @@ public class SyncedFolder implements Serializable, Cloneable {
      * @param remotePath      remote path
      * @param wifiOnly        upload on wifi only flag
      * @param chargingOnly    upload on charging only
+     * @param existing        upload existing files
      * @param subfolderByDate create sub-folders by date (month)
      * @param account         the account owning the synced folder
      * @param uploadAction    the action to be done after the upload
@@ -66,6 +68,7 @@ public class SyncedFolder implements Serializable, Cloneable {
                         String remotePath,
                         boolean wifiOnly,
                         boolean chargingOnly,
+                        boolean existing,
                         boolean subfolderByDate,
                         String account,
                         int uploadAction,
@@ -73,8 +76,8 @@ public class SyncedFolder implements Serializable, Cloneable {
                         long timestampMs,
                         MediaFolderType type,
                         boolean hidden) {
-        this(UNPERSISTED_ID, localPath, remotePath, wifiOnly, chargingOnly, subfolderByDate, account, uploadAction,
-             enabled, timestampMs, type, hidden);
+        this(UNPERSISTED_ID, localPath, remotePath, wifiOnly, chargingOnly, existing, subfolderByDate, account,
+             uploadAction, enabled, timestampMs, type, hidden);
     }
 
     /**
@@ -87,6 +90,7 @@ public class SyncedFolder implements Serializable, Cloneable {
                            String remotePath,
                            boolean wifiOnly,
                            boolean chargingOnly,
+                           boolean existing,
                            boolean subfolderByDate,
                            String account,
                            int uploadAction,
@@ -99,6 +103,7 @@ public class SyncedFolder implements Serializable, Cloneable {
         this.remotePath = remotePath;
         this.wifiOnly = wifiOnly;
         this.chargingOnly = chargingOnly;
+        this.existing = existing;
         this.subfolderByDate = subfolderByDate;
         this.account = account;
         this.uploadAction = uploadAction;

+ 7 - 4
src/main/java/com/owncloud/android/datamodel/SyncedFolderDisplayItem.java

@@ -45,6 +45,7 @@ public class SyncedFolderDisplayItem extends SyncedFolder {
      * @param remotePath      remote path
      * @param wifiOnly        upload on wifi only flag
      * @param chargingOnly    upload on charging only
+     * @param existing        also upload existing
      * @param subfolderByDate create sub-folders by date (month)
      * @param account         the account owning the synced folder
      * @param uploadAction    the action to be done after the upload
@@ -60,6 +61,7 @@ public class SyncedFolderDisplayItem extends SyncedFolder {
                                    String remotePath,
                                    boolean wifiOnly,
                                    boolean chargingOnly,
+                                   boolean existing,
                                    boolean subfolderByDate,
                                    String account,
                                    int uploadAction,
@@ -70,8 +72,8 @@ public class SyncedFolderDisplayItem extends SyncedFolder {
                                    long numberOfFiles,
                                    MediaFolderType type,
                                    boolean hidden) {
-        super(id, localPath, remotePath, wifiOnly, chargingOnly, subfolderByDate, account, uploadAction, enabled,
-              timestampMs, type, hidden);
+        super(id, localPath, remotePath, wifiOnly, chargingOnly, existing, subfolderByDate, account, uploadAction,
+              enabled, timestampMs, type, hidden);
         this.filePaths = filePaths;
         this.folderName = folderName;
         this.numberOfFiles = numberOfFiles;
@@ -82,14 +84,15 @@ public class SyncedFolderDisplayItem extends SyncedFolder {
                                    String remotePath,
                                    boolean wifiOnly,
                                    boolean chargingOnly,
+                                   boolean existing,
                                    boolean subfolderByDate,
                                    String account,
                                    int uploadAction,
                                    boolean enabled,
                                    long timestampMs,
                                    String folderName, MediaFolderType type, boolean hidden) {
-        super(id, localPath, remotePath, wifiOnly, chargingOnly, subfolderByDate, account, uploadAction, enabled,
-              timestampMs, type, hidden);
+        super(id, localPath, remotePath, wifiOnly, chargingOnly, existing, subfolderByDate, account, uploadAction,
+              enabled, timestampMs, type, hidden);
         this.folderName = folderName;
     }
 }

+ 6 - 2
src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java

@@ -342,6 +342,8 @@ public class SyncedFolderProvider extends Observable {
                     ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_WIFI_ONLY)) == 1;
             boolean chargingOnly = cursor.getInt(cursor.getColumnIndex(
                     ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_CHARGING_ONLY)) == 1;
+            boolean existing = cursor.getInt(cursor.getColumnIndex(
+                    ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_EXISTING)) == 1;
             boolean subfolderByDate = cursor.getInt(cursor.getColumnIndex(
                     ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_SUBFOLDER_BY_DATE)) == 1;
             String accountName = cursor.getString(cursor.getColumnIndex(
@@ -357,8 +359,9 @@ public class SyncedFolderProvider extends Observable {
             boolean hidden = cursor.getInt(cursor.getColumnIndex(
                 ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_HIDDEN)) == 1;
 
-            syncedFolder = new SyncedFolder(id, localPath, remotePath, wifiOnly, chargingOnly, subfolderByDate,
-                    accountName, uploadAction, enabled, enabledTimestampMs, type, hidden);
+            syncedFolder = new SyncedFolder(id, localPath, remotePath, wifiOnly, chargingOnly, existing,
+                                            subfolderByDate, accountName, uploadAction, enabled, enabledTimestampMs,
+                                            type, hidden);
         }
         return syncedFolder;
     }
@@ -376,6 +379,7 @@ public class SyncedFolderProvider extends Observable {
         cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_REMOTE_PATH, syncedFolder.getRemotePath());
         cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_WIFI_ONLY, syncedFolder.isWifiOnly());
         cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_CHARGING_ONLY, syncedFolder.isChargingOnly());
+        cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_EXISTING, syncedFolder.isExisting());
         cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ENABLED, syncedFolder.isEnabled());
         cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_ENABLED_TIMESTAMP_MS, syncedFolder.getEnabledTimestampMs());
         cv.put(ProviderMeta.ProviderTableMeta.SYNCED_FOLDER_SUBFOLDER_BY_DATE, syncedFolder.isSubfolderByDate());

+ 3 - 3
src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java

@@ -84,7 +84,7 @@ public class UploadsStorageManager extends Observable {
         cv.put(ProviderTableMeta.UPLOADS_FILE_SIZE, ocUpload.getFileSize());
         cv.put(ProviderTableMeta.UPLOADS_STATUS, ocUpload.getUploadStatus().value);
         cv.put(ProviderTableMeta.UPLOADS_LOCAL_BEHAVIOUR, ocUpload.getLocalAction());
-        cv.put(ProviderTableMeta.UPLOADS_FORCE_OVERWRITE, ocUpload.isForceOverwrite() ? 1 : 0);
+        cv.put(ProviderTableMeta.UPLOADS_NAME_COLLISION_POLICY, ocUpload.getNameCollisionPolicy().serialize());
         cv.put(ProviderTableMeta.UPLOADS_IS_CREATE_REMOTE_FOLDER, ocUpload.isCreateRemoteFolder() ? 1 : 0);
         cv.put(ProviderTableMeta.UPLOADS_LAST_RESULT, ocUpload.getLastResult().getValue());
         cv.put(ProviderTableMeta.UPLOADS_CREATED_BY, ocUpload.getCreatedBy());
@@ -329,8 +329,8 @@ public class UploadsStorageManager extends Observable {
                     UploadStatus.fromValue(c.getInt(c.getColumnIndex(ProviderTableMeta.UPLOADS_STATUS)))
             );
             upload.setLocalAction(c.getInt(c.getColumnIndex(ProviderTableMeta.UPLOADS_LOCAL_BEHAVIOUR)));
-            upload.setForceOverwrite(c.getInt(
-                    c.getColumnIndex(ProviderTableMeta.UPLOADS_FORCE_OVERWRITE)) == 1);
+            upload.setNameCollisionPolicy(FileUploader.NameCollisionPolicy.deserialize(c.getInt(
+                    c.getColumnIndex(ProviderTableMeta.UPLOADS_NAME_COLLISION_POLICY))));
             upload.setCreateRemoteFolder(c.getInt(
                     c.getColumnIndex(ProviderTableMeta.UPLOADS_IS_CREATE_REMOTE_FOLDER)) == 1);
             upload.setUploadEndTimestamp(c.getLong(c.getColumnIndex(ProviderTableMeta.UPLOADS_UPLOAD_END_TIMESTAMP)));

+ 5 - 5
src/main/java/com/owncloud/android/db/OCUpload.java

@@ -77,9 +77,9 @@ public class OCUpload implements Parcelable {
     @Getter @Setter private int localAction;
 
     /**
-     * Overwrite destination file?
+     * What to do in case of name collision.
      */
-    @Getter @Setter private boolean forceOverwrite;
+    @Getter @Setter private FileUploader.NameCollisionPolicy nameCollisionPolicy;
 
     /**
      * Create destination folder?
@@ -172,7 +172,7 @@ public class OCUpload implements Parcelable {
         fileSize = -1;
         uploadId = -1;
         localAction = FileUploader.LOCAL_BEHAVIOUR_COPY;
-        forceOverwrite = false;
+        nameCollisionPolicy = FileUploader.NameCollisionPolicy.DEFAULT;
         createRemoteFolder = false;
         uploadStatus = UploadStatus.UPLOAD_IN_PROGRESS;
         lastResult = UploadResult.UNKNOWN;
@@ -281,7 +281,7 @@ public class OCUpload implements Parcelable {
         remotePath = source.readString();
         accountName = source.readString();
         localAction = source.readInt();
-        forceOverwrite = source.readInt() == 1;
+        nameCollisionPolicy = FileUploader.NameCollisionPolicy.deserialize(source.readInt());
         createRemoteFolder = source.readInt() == 1;
         try {
             uploadStatus = UploadStatus.valueOf(source.readString());
@@ -312,7 +312,7 @@ public class OCUpload implements Parcelable {
         dest.writeString(remotePath);
         dest.writeString(accountName);
         dest.writeInt(localAction);
-        dest.writeInt(forceOverwrite ? 1 : 0);
+        dest.writeInt(nameCollisionPolicy.serialize());
         dest.writeInt(createRemoteFolder ? 1 : 0);
         dest.writeString(uploadStatus.name());
         dest.writeLong(uploadEndTimestamp);

+ 3 - 2
src/main/java/com/owncloud/android/db/ProviderMeta.java

@@ -31,7 +31,7 @@ import com.owncloud.android.MainApp;
  */
 public class ProviderMeta {
     public static final String DB_NAME = "filelist";
-    public static final int DB_VERSION = 53;
+    public static final int DB_VERSION = 54;
 
     private ProviderMeta() {
         // No instance
@@ -208,7 +208,7 @@ public class ProviderMeta {
         public static final String UPLOADS_STATUS = "status";
         public static final String UPLOADS_LOCAL_BEHAVIOUR = "local_behaviour";
         public static final String UPLOADS_UPLOAD_TIME = "upload_time";
-        public static final String UPLOADS_FORCE_OVERWRITE = "force_overwrite";
+        public static final String UPLOADS_NAME_COLLISION_POLICY = "name_collision_policy";
         public static final String UPLOADS_IS_CREATE_REMOTE_FOLDER = "is_create_remote_folder";
         public static final String UPLOADS_UPLOAD_END_TIMESTAMP = "upload_end_timestamp";
         public static final String UPLOADS_LAST_RESULT = "last_result";
@@ -223,6 +223,7 @@ public class ProviderMeta {
         public static final String SYNCED_FOLDER_REMOTE_PATH = "remote_path";
         public static final String SYNCED_FOLDER_WIFI_ONLY = "wifi_only";
         public static final String SYNCED_FOLDER_CHARGING_ONLY = "charging_only";
+        public static final String SYNCED_FOLDER_EXISTING = "existing";
         public static final String SYNCED_FOLDER_ENABLED = "enabled";
         public static final String SYNCED_FOLDER_ENABLED_TIMESTAMP_MS = "enabled_timestamp_ms";
         public static final String SYNCED_FOLDER_TYPE = "type";

+ 11 - 0
src/main/java/com/owncloud/android/files/services/FileDownloader.java

@@ -43,6 +43,8 @@ import com.owncloud.android.R;
 import com.owncloud.android.authentication.AuthenticatorActivity;
 import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.datamodel.UploadsStorageManager;
+import com.owncloud.android.db.OCUpload;
 import com.owncloud.android.lib.common.OwnCloudAccount;
 import com.owncloud.android.lib.common.OwnCloudClient;
 import com.owncloud.android.lib.common.OwnCloudClientManagerFactory;
@@ -87,6 +89,7 @@ public class FileDownloader extends Service
     public static final String EXTRA_FILE_PATH = "FILE_PATH";
     public static final String EXTRA_REMOTE_PATH = "REMOTE_PATH";
     public static final String EXTRA_LINKED_TO_PATH = "LINKED_TO";
+    public static final String EXTRA_CONFLICT_UPLOAD = "CONFLICT_UPLOAD";
     public static final String ACCOUNT_NAME = "ACCOUNT_NAME";
 
     private static final int FOREGROUND_SERVICE_ID = 412;
@@ -110,7 +113,10 @@ public class FileDownloader extends Service
 
     private Notification mNotification;
 
+    private OCUpload conflictUpload;
+
     @Inject UserAccountManager accountManager;
+    @Inject UploadsStorageManager uploadsStorageManager;
 
     public static String getDownloadAddedMessage() {
         return FileDownloader.class.getName() + DOWNLOAD_ADDED_MESSAGE;
@@ -195,6 +201,7 @@ public class FileDownloader extends Service
             final String behaviour = intent.getStringExtra(OCFileListFragment.DOWNLOAD_BEHAVIOUR);
             String activityName = intent.getStringExtra(SendShareDialog.ACTIVITY_NAME);
             String packageName = intent.getStringExtra(SendShareDialog.PACKAGE_NAME);
+            this.conflictUpload = intent.getParcelableExtra(FileDownloader.EXTRA_CONFLICT_UPLOAD);
             AbstractList<String> requestedDownloads = new Vector<String>();
             try {
                 DownloadFileOperation newDownload = new DownloadFileOperation(account, file, behaviour, activityName,
@@ -634,6 +641,10 @@ public class FileDownloader extends Service
 
                 // Remove success notification
                 if (downloadResult.isSuccess()) {
+                    if (this.conflictUpload != null) {
+                        uploadsStorageManager.removeUpload(this.conflictUpload);
+                    }
+
                     // Sleep 2 seconds, so show the notification before remove it
                     NotificationUtils.cancelWithDelay(mNotificationManager,
                             R.string.downloader_download_succeeded_ticker, 2000);

Failā izmaiņas netiks attēlotas, jo tās ir par lielu
+ 264 - 763
src/main/java/com/owncloud/android/files/services/FileUploader.java


+ 12 - 12
src/main/java/com/owncloud/android/jobs/ContactsBackupJob.java

@@ -169,18 +169,18 @@ public class ContactsBackupJob extends Job {
             }
         }
 
-        FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
-        requester.uploadNewFile(
-                getContext(),
-                user.toPlatformAccount(),
-                file.getAbsolutePath(),
-                backupFolder + filename,
-                FileUploader.LOCAL_BEHAVIOUR_MOVE,
-                null,
-                true,
-                UploadFileOperation.CREATED_BY_USER,
-                false,
-                false
+        FileUploader.uploadNewFile(
+            getContext(),
+            user.toPlatformAccount(),
+            file.getAbsolutePath(),
+            backupFolder + filename,
+            FileUploader.LOCAL_BEHAVIOUR_MOVE,
+            null,
+            true,
+            UploadFileOperation.CREATED_BY_USER,
+            false,
+            false,
+            FileUploader.NameCollisionPolicy.ASK_USER
         );
     }
 

+ 23 - 19
src/main/java/com/owncloud/android/jobs/FilesSyncJob.java

@@ -131,7 +131,7 @@ public class FilesSyncJob extends Job {
                                             userAccountManager,
                                             connectivityService,
                                             powerManagementService);
-        FilesSyncHelper.insertAllDBEntries(preferences, clock, skipCustom);
+        FilesSyncHelper.insertAllDBEntries(preferences, clock, skipCustom, false);
 
         // Create all the providers we'll need
         final ContentResolver contentResolver = context.getContentResolver();
@@ -141,12 +141,11 @@ public class FilesSyncJob extends Job {
         Locale currentLocale = context.getResources().getConfiguration().locale;
         SimpleDateFormat sFormatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", currentLocale);
         sFormatter.setTimeZone(TimeZone.getTimeZone(TimeZone.getDefault().getID()));
-        FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
 
         for (SyncedFolder syncedFolder : syncedFolderProvider.getSyncedFolders()) {
             if ((syncedFolder.isEnabled()) && (!skipCustom || MediaFolderType.CUSTOM != syncedFolder.getType())) {
                 syncFolder(context, resources, lightVersion, filesystemDataProvider, currentLocale, sFormatter,
-                        requester, syncedFolder);
+                           syncedFolder);
             }
         }
 
@@ -157,10 +156,15 @@ public class FilesSyncJob extends Job {
         return Result.SUCCESS;
     }
 
-    private void syncFolder(Context context, Resources resources, boolean lightVersion,
-                            FilesystemDataProvider filesystemDataProvider, Locale currentLocale,
-                            SimpleDateFormat sFormatter, FileUploader.UploadRequester requester,
-                            SyncedFolder syncedFolder) {
+    private void syncFolder(
+        Context context,
+        Resources resources,
+        boolean lightVersion,
+        FilesystemDataProvider filesystemDataProvider,
+        Locale currentLocale,
+        SimpleDateFormat sFormatter,
+        SyncedFolder syncedFolder
+    ) {
         String remotePath;
         boolean subfolderByDate;
         Integer uploadAction;
@@ -203,24 +207,24 @@ public class FilesSyncJob extends Job {
                 remotePath = syncedFolder.getRemotePath();
             }
 
-            requester.uploadFileWithOverwrite(
-                    context,
-                    user.toPlatformAccount(),
-                    file.getAbsolutePath(),
-                    FileStorageUtils.getInstantUploadFilePath(
+            FileUploader.uploadNewFile(
+                context,
+                user.toPlatformAccount(),
+                file.getAbsolutePath(),
+                FileStorageUtils.getInstantUploadFilePath(
                         file,
                         currentLocale,
                         remotePath,
                         syncedFolder.getLocalPath(),
                         lastModificationTime,
                         subfolderByDate),
-                    uploadAction,
-                    mimeType,
-                    true,           // create parent folder if not existent
-                    UploadFileOperation.CREATED_AS_INSTANT_PICTURE,
-                    needsWifi,
-                    needsCharging,
-                    true
+                uploadAction,
+                mimeType,
+                true,           // create parent folder if not existent
+                UploadFileOperation.CREATED_AS_INSTANT_PICTURE,
+                needsWifi,
+                needsCharging,
+                FileUploader.NameCollisionPolicy.ASK_USER
             );
 
             filesystemDataProvider.updateFilesystemFileAsSentForUpload(path,

+ 7 - 2
src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java

@@ -291,8 +291,13 @@ public class SynchronizeFileOperation extends SyncOperation {
      * @param file OCFile object representing the file to upload
      */
     private void requestForUpload(OCFile file) {
-        FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
-        requester.uploadUpdate(mContext, mAccount, file, FileUploader.LOCAL_BEHAVIOUR_MOVE, true);
+        FileUploader.uploadUpdateFile(
+            mContext,
+            mAccount,
+            file,
+            FileUploader.LOCAL_BEHAVIOUR_MOVE,
+            FileUploader.NameCollisionPolicy.ASK_USER
+        );
 
         mTransferWasRequested = true;
     }

+ 57 - 44
src/main/java/com/owncloud/android/operations/UploadFileOperation.java

@@ -90,6 +90,7 @@ import java.util.Set;
 import java.util.UUID;
 import java.util.concurrent.atomic.AtomicBoolean;
 
+import androidx.annotation.CheckResult;
 import androidx.annotation.RequiresApi;
 
 
@@ -111,14 +112,14 @@ public class UploadFileOperation extends SyncOperation {
     private OCFile mFile;
 
     /**
-     * Original OCFile which is to be uploaded in case file had to be renamed
-     * (if forceOverwrite==false and remote file already exists).
+     * Original OCFile which is to be uploaded in case file had to be renamed (if nameCollisionPolicy==RENAME and remote
+     * file already exists).
      */
     private OCFile mOldFile;
     private String mRemotePath;
     private String mFolderUnlockToken;
     private boolean mRemoteFolderToBeCreated;
-    private boolean mForceOverwrite;
+    private FileUploader.NameCollisionPolicy mNameCollisionPolicy;
     private int mLocalBehaviour;
     private int mCreatedBy;
     private boolean mOnWifiOnly;
@@ -183,7 +184,7 @@ public class UploadFileOperation extends SyncOperation {
                                Account account,
                                OCFile file,
                                OCUpload upload,
-                               boolean forceOverwrite,
+                               FileUploader.NameCollisionPolicy nameCollisionPolicy,
                                int localBehaviour,
                                Context context,
                                boolean onWifiOnly,
@@ -218,7 +219,7 @@ public class UploadFileOperation extends SyncOperation {
         mOnWifiOnly = onWifiOnly;
         mWhileChargingOnly = whileChargingOnly;
         mRemotePath = upload.getRemotePath();
-        mForceOverwrite = forceOverwrite;
+        mNameCollisionPolicy = nameCollisionPolicy;
         mLocalBehaviour = localBehaviour;
         mOriginalStoragePath = mFile.getStoragePath();
         mContext = context;
@@ -504,7 +505,11 @@ public class UploadFileOperation extends SyncOperation {
             /**** E2E *****/
 
             // check name collision
-            checkNameCollision(client, metadata, parentFile.isEncrypted());
+            RemoteOperationResult collisionResult = checkNameCollision(client, metadata, parentFile.isEncrypted());
+            if (collisionResult != null) {
+                result = collisionResult;
+                return collisionResult;
+            }
 
             String expectedPath = FileStorageUtils.getDefaultSavePathFor(mAccount.name, mFile);
             expectedFile = new File(expectedPath);
@@ -759,7 +764,11 @@ public class UploadFileOperation extends SyncOperation {
             }
 
             // check name collision
-            checkNameCollision(client, null, false);
+            RemoteOperationResult collisionResult = checkNameCollision(client, null, false);
+            if (collisionResult != null) {
+                result = collisionResult;
+                return collisionResult;
+            }
 
             String expectedPath = FileStorageUtils.getDefaultSavePathFor(mAccount.name, mFile);
             expectedFile = new File(expectedPath);
@@ -922,24 +931,37 @@ public class UploadFileOperation extends SyncOperation {
         return new RemoteOperationResult(ResultCode.OK);
     }
 
-    private void checkNameCollision(OwnCloudClient client, DecryptedFolderMetadata metadata, boolean encrypted)
-            throws OperationCancelledException {
-        /// automatic rename of file to upload in case of name collision in server
+    @CheckResult
+    private RemoteOperationResult checkNameCollision(OwnCloudClient client, DecryptedFolderMetadata metadata, boolean encrypted)
+        throws OperationCancelledException {
         Log_OC.d(TAG, "Checking name collision in server");
-        if (!mForceOverwrite) {
-            String remotePath = getAvailableRemotePath(client, mRemotePath, metadata, encrypted);
-            mWasRenamed = !remotePath.equals(mRemotePath);
-            if (mWasRenamed) {
-                createNewOCFile(remotePath);
-                Log_OC.d(TAG, "File renamed as " + remotePath);
+
+        if (existsFile(client, mRemotePath, metadata, encrypted)) {
+            switch (mNameCollisionPolicy) {
+                case CANCEL:
+                    Log_OC.d(TAG, "File exists; canceling");
+                    throw new OperationCancelledException();
+                case RENAME:
+                    mRemotePath = getNewAvailableRemotePath(client, mRemotePath, metadata, encrypted);
+                    mWasRenamed = true;
+                    createNewOCFile(mRemotePath);
+                    Log_OC.d(TAG, "File renamed as " + mRemotePath);
+                    mRenameUploadListener.onRenameUpload();
+                    break;
+                case OVERWRITE:
+                    Log_OC.d(TAG, "Overwriting file");
+                    break;
+                case ASK_USER:
+                    Log_OC.d(TAG, "Name collision; asking the user what to do");
+                    return new RemoteOperationResult(ResultCode.SYNC_CONFLICT);
             }
-            mRemotePath = remotePath;
-            mRenameUploadListener.onRenameUpload();
         }
 
         if (mCancellationRequested.get()) {
             throw new OperationCancelledException();
         }
+
+        return null;
     }
 
     private void handleSuccessfulUpload(File temporalFile, File expectedFile, File originalFile,
@@ -1043,8 +1065,8 @@ public class UploadFileOperation extends SyncOperation {
 
 
     /**
-     * Create a new OCFile mFile with new remote path. This is required if forceOverwrite==false.
-     * New file is stored as mFile, original as mOldFile.
+     * Create a new OCFile mFile with new remote path. This is required if nameCollisionPolicy==RENAME. New file is
+     * stored as mFile, original as mOldFile.
      *
      * @param newRemotePath new remote path
      */
@@ -1068,45 +1090,36 @@ public class UploadFileOperation extends SyncOperation {
     }
 
     /**
-     * Checks if remotePath does not exist in the server and returns it, or adds
-     * a suffix to it in order to avoid the server file is overwritten.
+     * Returns a new and available (does not exists on the server) remotePath.
+     * This adds an incremental suffix.
      *
      * @param client     OwnCloud client
      * @param remotePath remote path of the file
      * @param metadata   metadata of encrypted folder
      * @return new remote path
      */
-    private String getAvailableRemotePath(OwnCloudClient client, String remotePath, DecryptedFolderMetadata metadata,
-                                          boolean encrypted) {
-        boolean check = existsFile(client, remotePath, metadata, encrypted);
-        if (!check) {
-            return remotePath;
-        }
-
-        int pos = remotePath.lastIndexOf('.');
+    private String getNewAvailableRemotePath(OwnCloudClient client, String remotePath, DecryptedFolderMetadata metadata,
+                                             boolean encrypted) {
+        int extPos = remotePath.lastIndexOf('.');
         String suffix;
         String extension = "";
         String remotePathWithoutExtension = "";
-        if (pos >= 0) {
-            extension = remotePath.substring(pos + 1);
-            remotePathWithoutExtension = remotePath.substring(0, pos);
+        if (extPos >= 0) {
+            extension = remotePath.substring(extPos + 1);
+            remotePathWithoutExtension = remotePath.substring(0, extPos);
         }
+
         int count = 2;
+        boolean exists;
+        String newPath;
         do {
             suffix = " (" + count + ")";
-            if (pos >= 0) {
-                check = existsFile(client, remotePathWithoutExtension + suffix + "." + extension, metadata, encrypted);
-            } else {
-                check = existsFile(client, remotePath + suffix, metadata, encrypted);
-            }
+            newPath = extPos >= 0 ? remotePathWithoutExtension + suffix + "." + extension : remotePath + suffix;
+            exists = existsFile(client, newPath, metadata, encrypted);
             count++;
-        } while (check);
+        } while (exists);
 
-        if (pos >= 0) {
-            return remotePathWithoutExtension + suffix + "." + extension;
-        } else {
-            return remotePath + suffix;
-        }
+        return newPath;
     }
 
     private boolean existsFile(OwnCloudClient client, String remotePath, DecryptedFolderMetadata metadata,

+ 65 - 1
src/main/java/com/owncloud/android/providers/FileContentProvider.java

@@ -804,7 +804,7 @@ public class FileContentProvider extends ContentProvider {
                        + ProviderTableMeta.UPLOADS_STATUS + INTEGER               // UploadStatus
                        + ProviderTableMeta.UPLOADS_LOCAL_BEHAVIOUR + INTEGER      // Upload LocalBehaviour
                        + ProviderTableMeta.UPLOADS_UPLOAD_TIME + INTEGER
-                       + ProviderTableMeta.UPLOADS_FORCE_OVERWRITE + INTEGER  // boolean
+                       + ProviderTableMeta.UPLOADS_NAME_COLLISION_POLICY + INTEGER  // boolean
                        + ProviderTableMeta.UPLOADS_IS_CREATE_REMOTE_FOLDER + INTEGER  // boolean
                        + ProviderTableMeta.UPLOADS_UPLOAD_END_TIMESTAMP + INTEGER
                        + ProviderTableMeta.UPLOADS_LAST_RESULT + INTEGER     // Upload LastResult
@@ -830,6 +830,7 @@ public class FileContentProvider extends ContentProvider {
                        + ProviderTableMeta.SYNCED_FOLDER_REMOTE_PATH + " TEXT, "          // remote path
                        + ProviderTableMeta.SYNCED_FOLDER_WIFI_ONLY + " INTEGER, "         // wifi_only
                        + ProviderTableMeta.SYNCED_FOLDER_CHARGING_ONLY + " INTEGER, "     // charging only
+                       + ProviderTableMeta.SYNCED_FOLDER_EXISTING + " INTEGER, "          // existing
                        + ProviderTableMeta.SYNCED_FOLDER_ENABLED + " INTEGER, "           // enabled
                        + ProviderTableMeta.SYNCED_FOLDER_ENABLED_TIMESTAMP_MS + " INTEGER, "           // enable date
                        + ProviderTableMeta.SYNCED_FOLDER_SUBFOLDER_BY_DATE + " INTEGER, " // subfolder by date
@@ -2104,6 +2105,69 @@ public class FileContentProvider extends ContentProvider {
             if (!upgraded) {
                 Log_OC.i(SQL, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
             }
+
+            if (oldVersion < 54 && newVersion >= 54) {
+                Log_OC.i(SQL, "Entering in the #54 add synced.existing," +
+                    " rename uploads.force_overwrite to uploads.name_collision_policy");
+                db.beginTransaction();
+                try {
+                    // Add synced.existing
+                    db.execSQL(ALTER_TABLE + ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME +
+                                   ADD_COLUMN + ProviderTableMeta.SYNCED_FOLDER_EXISTING + " INTEGER "); // boolean
+
+
+                    // Rename uploads.force_overwrite to uploads.name_collision_policy
+                    String tmpTableName = ProviderTableMeta.UPLOADS_TABLE_NAME + "_old";
+                    db.execSQL(ALTER_TABLE + ProviderTableMeta.UPLOADS_TABLE_NAME + " RENAME TO " + tmpTableName);
+                    createUploadsTable(db);
+                    db.execSQL("INSERT INTO " + ProviderTableMeta.UPLOADS_TABLE_NAME + " (" +
+                                   ProviderTableMeta._ID + ", " +
+                                   ProviderTableMeta.UPLOADS_LOCAL_PATH + ", " +
+                                   ProviderTableMeta.UPLOADS_REMOTE_PATH + ", " +
+                                   ProviderTableMeta.UPLOADS_ACCOUNT_NAME + ", " +
+                                   ProviderTableMeta.UPLOADS_FILE_SIZE + ", " +
+                                   ProviderTableMeta.UPLOADS_STATUS + ", " +
+                                   ProviderTableMeta.UPLOADS_LOCAL_BEHAVIOUR + ", " +
+                                   ProviderTableMeta.UPLOADS_UPLOAD_TIME + ", " +
+                                   ProviderTableMeta.UPLOADS_NAME_COLLISION_POLICY + ", " +
+                                   ProviderTableMeta.UPLOADS_IS_CREATE_REMOTE_FOLDER + ", " +
+                                   ProviderTableMeta.UPLOADS_UPLOAD_END_TIMESTAMP + ", " +
+                                   ProviderTableMeta.UPLOADS_LAST_RESULT + ", " +
+                                   ProviderTableMeta.UPLOADS_IS_WHILE_CHARGING_ONLY + ", " +
+                                   ProviderTableMeta.UPLOADS_IS_WIFI_ONLY + ", " +
+                                   ProviderTableMeta.UPLOADS_CREATED_BY + ", " +
+                                   ProviderTableMeta.UPLOADS_FOLDER_UNLOCK_TOKEN +
+                                   ") " +
+                                   " SELECT " +
+                                   ProviderTableMeta._ID + ", " +
+                                   ProviderTableMeta.UPLOADS_LOCAL_PATH + ", " +
+                                   ProviderTableMeta.UPLOADS_REMOTE_PATH + ", " +
+                                   ProviderTableMeta.UPLOADS_ACCOUNT_NAME + ", " +
+                                   ProviderTableMeta.UPLOADS_FILE_SIZE + ", " +
+                                   ProviderTableMeta.UPLOADS_STATUS + ", " +
+                                   ProviderTableMeta.UPLOADS_LOCAL_BEHAVIOUR + ", " +
+                                   ProviderTableMeta.UPLOADS_UPLOAD_TIME + ", " +
+                                   "force_overwrite" + ", " + // See FileUploader.NameCollisionPolicy
+                                   ProviderTableMeta.UPLOADS_IS_CREATE_REMOTE_FOLDER + ", " +
+                                   ProviderTableMeta.UPLOADS_UPLOAD_END_TIMESTAMP + ", " +
+                                   ProviderTableMeta.UPLOADS_LAST_RESULT + ", " +
+                                   ProviderTableMeta.UPLOADS_IS_WHILE_CHARGING_ONLY + ", " +
+                                   ProviderTableMeta.UPLOADS_IS_WIFI_ONLY + ", " +
+                                   ProviderTableMeta.UPLOADS_CREATED_BY + ", " +
+                                   ProviderTableMeta.UPLOADS_FOLDER_UNLOCK_TOKEN +
+                                   " FROM " + tmpTableName);
+                    db.execSQL("DROP TABLE " + tmpTableName);
+
+                    upgraded = true;
+                    db.setTransactionSuccessful();
+                } finally {
+                    db.endTransaction();
+                }
+            }
+
+            if (!upgraded) {
+                Log_OC.i(SQL, String.format(Locale.ENGLISH, UPGRADE_VERSION_MSG, oldVersion, newVersion));
+            }
         }
     }
 }

+ 81 - 34
src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.java

@@ -25,6 +25,8 @@ import android.content.Intent;
 import android.os.Bundle;
 
 import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.datamodel.UploadsStorageManager;
+import com.owncloud.android.db.OCUpload;
 import com.owncloud.android.files.services.FileDownloader;
 import com.owncloud.android.files.services.FileUploader;
 import com.owncloud.android.lib.common.utils.Log_OC;
@@ -32,52 +34,96 @@ import com.owncloud.android.ui.dialog.ConflictsResolveDialog;
 import com.owncloud.android.ui.dialog.ConflictsResolveDialog.Decision;
 import com.owncloud.android.ui.dialog.ConflictsResolveDialog.OnConflictDecisionMadeListener;
 
+import javax.inject.Inject;
+
 
 /**
  * Wrapper activity which will be launched if keep-in-sync file will be modified by external
  * application.
  */
 public class ConflictsResolveActivity extends FileActivity implements OnConflictDecisionMadeListener {
+    /**
+     * A nullable upload entry that must be removed when and if the conflict is resolved.
+     */
+    public static final String EXTRA_CONFLICT_UPLOAD = "CONFLICT_UPLOAD";
+    /**
+     * Specify the upload local behaviour when there is no CONFLICT_UPLOAD.
+     */
+    public static final String EXTRA_LOCAL_BEHAVIOUR = "LOCAL_BEHAVIOUR";
 
     private static final String TAG = ConflictsResolveActivity.class.getSimpleName();
 
+    @Inject UploadsStorageManager uploadsStorageManager;
+
+    private OCUpload conflictUpload;
+    private int localBehaviour = FileUploader.LOCAL_BEHAVIOUR_FORGET;
+
     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+
+        if (savedInstanceState != null) {
+            conflictUpload = savedInstanceState.getParcelable(EXTRA_CONFLICT_UPLOAD);
+            localBehaviour = savedInstanceState.getInt(EXTRA_LOCAL_BEHAVIOUR);
+        } else {
+            conflictUpload = getIntent().getParcelableExtra(EXTRA_CONFLICT_UPLOAD);
+            localBehaviour = getIntent().getIntExtra(EXTRA_LOCAL_BEHAVIOUR, localBehaviour);
+        }
+
+        if (conflictUpload != null) {
+            localBehaviour = conflictUpload.getLocalAction();
+        }
     }
 
     @Override
     public void conflictDecisionMade(Decision decision) {
+        if (decision == Decision.CANCEL) {
+            return;
+        }
 
-        Integer behaviour = null;
-        Boolean forceOverwrite = null;
+        OCFile file = getFile();
 
         switch (decision) {
-            case CANCEL:
-                finish();
-                return;
-            case OVERWRITE:
-                // use local version -> overwrite on server
-                forceOverwrite = true;
+            case KEEP_LOCAL: // Upload
+                FileUploader.uploadUpdateFile(
+                    this,
+                    getAccount(),
+                    file,
+                    localBehaviour,
+                    FileUploader.NameCollisionPolicy.OVERWRITE
+                );
+
+                if (conflictUpload != null) {
+                    uploadsStorageManager.removeUpload(conflictUpload);
+                }
                 break;
-            case KEEP_BOTH:
-                behaviour = FileUploader.LOCAL_BEHAVIOUR_MOVE;
+            case KEEP_BOTH: // Upload
+                FileUploader.uploadUpdateFile(
+                    this,
+                    getAccount(),
+                    file,
+                    localBehaviour,
+                    FileUploader.NameCollisionPolicy.RENAME
+                );
+
+                if (conflictUpload != null) {
+                    uploadsStorageManager.removeUpload(conflictUpload);
+                }
+                break;
+            case KEEP_SERVER: // Download
+                if (!this.shouldDeleteLocal()) {
+                    // Overwrite local file
+                    Intent intent = new Intent(this, FileDownloader.class);
+                    intent.putExtra(FileDownloader.EXTRA_ACCOUNT, getAccount());
+                    intent.putExtra(FileDownloader.EXTRA_FILE, file);
+                    if (conflictUpload != null) {
+                        intent.putExtra(FileDownloader.EXTRA_CONFLICT_UPLOAD, conflictUpload);
+                    }
+                    startService(intent);
+                }
                 break;
-            case SERVER:
-                // use server version -> delete local, request download
-                Intent intent = new Intent(this, FileDownloader.class);
-                intent.putExtra(FileDownloader.EXTRA_ACCOUNT, getAccount());
-                intent.putExtra(FileDownloader.EXTRA_FILE, getFile());
-                startService(intent);
-                finish();
-                return;
-            default:
-                Log_OC.e(TAG, "Unhandled conflict decision " + decision);
-                return;
         }
 
-        FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
-        requester.uploadUpdate(this, getAccount(), getFile(), behaviour, forceOverwrite);
         finish();
     }
 
@@ -87,26 +133,27 @@ public class ConflictsResolveActivity extends FileActivity implements OnConflict
         if (getAccount() != null) {
             OCFile file = getFile();
             if (getFile() == null) {
-                Log_OC.e(TAG, "No conflictive file received");
+                Log_OC.e(TAG, "No file received");
                 finish();
             } else {
-                /// Check whether the 'main' OCFile handled by the Activity is contained in the current Account
-                file = getStorageManager().getFileByPath(file.getRemotePath());   // file = null if not in the
-                // current Account
-                if (file != null) {
-                    setFile(file);
-                    ConflictsResolveDialog d = ConflictsResolveDialog.newInstance(this);
-                    d.showDialog(this);
-
+                // Check whether the file is contained in the current Account
+                if (getStorageManager().fileExists(file.getRemotePath())) {
+                    ConflictsResolveDialog dialog = new ConflictsResolveDialog(this, !this.shouldDeleteLocal());
+                    dialog.showDialog(this);
                 } else {
-                    // account was changed to a different one - just finish
+                    // Account was changed to a different one - just finish
                     finish();
                 }
             }
-
         } else {
             finish();
         }
+    }
 
+    /**
+     * @return whether the local version of the files is to be deleted.
+     */
+    private boolean shouldDeleteLocal() {
+        return localBehaviour == FileUploader.LOCAL_BEHAVIOUR_DELETE;
     }
 }

+ 3 - 3
src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java

@@ -1043,8 +1043,7 @@ public class FileDisplayActivity extends FileActivity
                     break;
             }
 
-            FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
-            requester.uploadNewFile(
+            FileUploader.uploadNewFile(
                 this,
                 getAccount(),
                 filePaths,
@@ -1054,7 +1053,8 @@ public class FileDisplayActivity extends FileActivity
                 false,          // do not create parent folder if not existent
                 UploadFileOperation.CREATED_BY_USER,
                 false,
-                false
+                false,
+                FileUploader.NameCollisionPolicy.ASK_USER
             );
 
         } else {

+ 5 - 5
src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java

@@ -888,19 +888,19 @@ public class ReceiveExternalFilesActivity extends FileActivity
     }
 
     public void uploadFile(String tmpName, String filename) {
-        FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
-        requester.uploadNewFile(
+        FileUploader.uploadNewFile(
             getBaseContext(),
             getAccount(),
-                tmpName,
+            tmpName,
             mFile.getRemotePath() + filename,
             FileUploader.LOCAL_BEHAVIOUR_COPY,
             null,
             true,
             UploadFileOperation.CREATED_BY_USER,
             false,
-            false
-            );
+            false,
+            FileUploader.NameCollisionPolicy.ASK_USER
+        );
         finish();
     }
 

+ 18 - 10
src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.java

@@ -416,6 +416,7 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
                 syncedFolder.getRemotePath(),
                 syncedFolder.isWifiOnly(),
                 syncedFolder.isChargingOnly(),
+                syncedFolder.isExisting(),
                 syncedFolder.isSubfolderByDate(),
                 syncedFolder.getAccount(),
                 syncedFolder.getUploadAction(),
@@ -443,6 +444,7 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
                 syncedFolder.getRemotePath(),
                 syncedFolder.isWifiOnly(),
                 syncedFolder.isChargingOnly(),
+                syncedFolder.isExisting(),
                 syncedFolder.isSubfolderByDate(),
                 syncedFolder.getAccount(),
                 syncedFolder.getUploadAction(),
@@ -469,6 +471,7 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
                 getString(R.string.instant_upload_path) + "/" + mediaFolder.folderName,
                 true,
                 false,
+                true,
                 false,
                 getAccount().name,
                 FileUploader.LOCAL_BEHAVIOUR_FORGET,
@@ -577,7 +580,7 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
             case R.id.action_create_custom_folder: {
                 Log.d(TAG, "Show custom folder dialog");
                 SyncedFolderDisplayItem emptyCustomFolder = new SyncedFolderDisplayItem(
-                    SyncedFolder.UNPERSISTED_ID, null, null, true, false,
+                    SyncedFolder.UNPERSISTED_ID, null, null, true, false, true,
                     false, getAccount().name, FileUploader.LOCAL_BEHAVIOUR_FORGET, false,
                     clock.getCurrentTime(), null, MediaFolderType.CUSTOM, false);
                 onSyncFolderSettingsClick(0, emptyCustomFolder);
@@ -619,7 +622,7 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
         }
 
         if (syncedFolderDisplayItem.isEnabled()) {
-            FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolderDisplayItem);
+            FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolderDisplayItem, true);
 
             showBatteryOptimizationInfo();
         }
@@ -709,18 +712,20 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
         if (MediaFolderType.CUSTOM == syncedFolder.getType() && syncedFolder.getId() == UNPERSISTED_ID) {
             SyncedFolderDisplayItem newCustomFolder = new SyncedFolderDisplayItem(
                     SyncedFolder.UNPERSISTED_ID, syncedFolder.getLocalPath(), syncedFolder.getRemotePath(),
-                    syncedFolder.isWifiOnly(), syncedFolder.isChargingOnly(), syncedFolder.isSubfolderByDate(),
-                    syncedFolder.getAccount(), syncedFolder.getUploadAction(), syncedFolder.isEnabled(),
-                    clock.getCurrentTime(), new File(syncedFolder.getLocalPath()).getName(), syncedFolder.getType(), syncedFolder.isHidden());
+                    syncedFolder.isWifiOnly(), syncedFolder.isChargingOnly(),
+                    syncedFolder.isExisting(), syncedFolder.isSubfolderByDate(), syncedFolder.getAccount(),
+                    syncedFolder.getUploadAction(), syncedFolder.isEnabled(), clock.getCurrentTime(),
+                    new File(syncedFolder.getLocalPath()).getName(), syncedFolder.getType(), syncedFolder.isHidden());
 
             saveOrUpdateSyncedFolder(newCustomFolder);
             adapter.addSyncFolderItem(newCustomFolder);
         } else {
             SyncedFolderDisplayItem item = adapter.get(syncedFolder.getSection());
             updateSyncedFolderItem(item, syncedFolder.getId(), syncedFolder.getLocalPath(),
-                                   syncedFolder.getRemotePath(), syncedFolder
-                    .isWifiOnly(), syncedFolder.isChargingOnly(), syncedFolder.isSubfolderByDate(), syncedFolder
-                    .getUploadAction(), syncedFolder.isEnabled());
+                                   syncedFolder.getRemotePath(), syncedFolder.isWifiOnly(),
+                                   syncedFolder.isChargingOnly(), syncedFolder.isExisting(),
+                                   syncedFolder.isSubfolderByDate(), syncedFolder.getUploadAction(),
+                                   syncedFolder.isEnabled());
 
             saveOrUpdateSyncedFolder(item);
 
@@ -743,7 +748,7 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
             // existing synced folder setup to be updated
             syncedFolderProvider.updateSyncFolder(item);
             if (item.isEnabled()) {
-                FilesSyncHelper.insertAllDBEntriesForSyncedFolder(item);
+                FilesSyncHelper.insertAllDBEntriesForSyncedFolder(item, true);
             } else {
                 String syncedFolderInitiatedKey = "syncedFolderIntitiated_" + item.getId();
 
@@ -761,7 +766,7 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
         if (storedId != -1) {
             item.setId(storedId);
             if (item.isEnabled()) {
-                FilesSyncHelper.insertAllDBEntriesForSyncedFolder(item);
+                FilesSyncHelper.insertAllDBEntriesForSyncedFolder(item, true);
             } else {
                 String syncedFolderInitiatedKey = "syncedFolderIntitiated_" + item.getId();
                 arbitraryDataProvider.deleteKeyForAccount("global", syncedFolderInitiatedKey);
@@ -788,6 +793,7 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
      * @param remotePath      the remote path
      * @param wifiOnly        upload on wifi only
      * @param chargingOnly    upload on charging only
+     * @param existing        also upload existing
      * @param subfolderByDate created sub folders
      * @param uploadAction    upload action
      * @param enabled         is sync enabled
@@ -798,6 +804,7 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
                                                            String remotePath,
                                                            boolean wifiOnly,
                                                            boolean chargingOnly,
+                                                           boolean existing,
                                                            boolean subfolderByDate,
                                                            Integer uploadAction,
                                                            boolean enabled) {
@@ -806,6 +813,7 @@ public class SyncedFoldersActivity extends FileActivity implements SyncedFolderA
         item.setRemotePath(remotePath);
         item.setWifiOnly(wifiOnly);
         item.setChargingOnly(chargingOnly);
+        item.setExisting(existing);
         item.setSubfolderByDate(subfolderByDate);
         item.setUploadAction(uploadAction);
         item.setEnabled(enabled, clock.getCurrentTime());

+ 16 - 9
src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java

@@ -45,6 +45,7 @@ import com.evernote.android.job.JobRequest;
 import com.evernote.android.job.util.support.PersistableBundleCompat;
 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.network.ConnectivityService;
 import com.nextcloud.java.util.Optional;
@@ -121,6 +122,9 @@ public class UploadListActivity extends FileActivity {
     @Inject
     PowerManagementService powerManagementService;
 
+    @Inject
+    Clock clock;
+
     @Override
     public void showFiles(boolean onDeviceOnly) {
         super.showFiles(onDeviceOnly);
@@ -169,9 +173,11 @@ public class UploadListActivity extends FileActivity {
 
         uploadListAdapter = new UploadListAdapter(this,
                                                   uploadsStorageManager,
+                                                  getStorageManager(),
                                                   userAccountManager,
                                                   connectivityService,
-                                                  powerManagementService);
+                                                  powerManagementService,
+                                                  clock);
 
         final GridLayoutManager lm = new GridLayoutManager(this, 1);
         uploadListAdapter.setLayoutManager(lm);
@@ -214,14 +220,15 @@ public class UploadListActivity extends FileActivity {
         }
 
         // retry failed uploads
-        FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
-        new Thread(() -> requester.retryFailedUploads(this,
-                                                      null,
-                                                      uploadsStorageManager,
-                                                      connectivityService,
-                                                      userAccountManager,
-                                                      powerManagementService,
-                                                      null)).start();
+        new Thread(() -> FileUploader.retryFailedUploads(
+            this,
+            null,
+            uploadsStorageManager,
+            connectivityService,
+            userAccountManager,
+            powerManagementService,
+            null
+        )).start();
 
         // update UI
         uploadListAdapter.loadUploadItemsFromDb();

+ 147 - 39
src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java

@@ -26,6 +26,7 @@ package com.owncloud.android.ui.adapter;
 
 import android.accounts.Account;
 import android.content.ActivityNotFoundException;
+import android.content.Context;
 import android.content.Intent;
 import android.graphics.Bitmap;
 import android.graphics.drawable.Drawable;
@@ -37,6 +38,7 @@ import android.view.ViewGroup;
 import android.widget.ImageButton;
 import android.widget.ImageView;
 import android.widget.LinearLayout;
+import android.widget.PopupMenu;
 import android.widget.ProgressBar;
 import android.widget.TextView;
 
@@ -44,10 +46,13 @@ 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.network.ConnectivityService;
 import com.nextcloud.java.util.Optional;
+import com.owncloud.android.MainApp;
 import com.owncloud.android.R;
+import com.owncloud.android.datamodel.FileDataStorageManager;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.ThumbnailsCacheManager;
 import com.owncloud.android.datamodel.UploadsStorageManager;
@@ -56,7 +61,10 @@ import com.owncloud.android.db.OCUpload;
 import com.owncloud.android.db.OCUploadComparator;
 import com.owncloud.android.db.UploadResult;
 import com.owncloud.android.files.services.FileUploader;
+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.utils.DisplayUtils;
 import com.owncloud.android.utils.MimeTypeUtil;
@@ -78,9 +86,11 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
     private ProgressListener progressListener;
     private FileActivity parentActivity;
     private UploadsStorageManager uploadsStorageManager;
+    private FileDataStorageManager storageManager;
     private ConnectivityService connectivityService;
     private PowerManagementService powerManagementService;
     private UserAccountManager accountManager;
+    private Clock clock;
     private UploadGroup[] uploadGroups;
     private boolean showUser;
 
@@ -133,16 +143,15 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
                     uploadsStorageManager.clearSuccessfulUploads();
                     break;
                 case FAILED:
-                    new Thread(() -> new FileUploader.UploadRequester()
-                        .retryFailedUploads(
-                            parentActivity,
-                            null,
-                            uploadsStorageManager,
-                            connectivityService,
-                            accountManager,
-                            powerManagementService,
-                            null))
-                        .start();
+                    new Thread(() -> FileUploader.retryFailedUploads(
+                        parentActivity,
+                        null,
+                        uploadsStorageManager,
+                        connectivityService,
+                        accountManager,
+                        powerManagementService,
+                        null
+                    )).start();
                     break;
 
                 default:
@@ -161,15 +170,19 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
 
     public UploadListAdapter(final FileActivity fileActivity,
                              final UploadsStorageManager uploadsStorageManager,
+                             final FileDataStorageManager storageManager,
                              final UserAccountManager accountManager,
                              final ConnectivityService connectivityService,
-                             final PowerManagementService powerManagementService) {
+                             final PowerManagementService powerManagementService,
+                             final Clock clock) {
         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;
         uploadGroups = new UploadGroup[3];
 
         shouldShowHeadersForEmptySections(false);
@@ -323,14 +336,22 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
             });
 
         } else if (item.getUploadStatus() == UploadStatus.UPLOAD_FAILED) {
-            // Delete
-            itemViewHolder.button.setImageResource(R.drawable.ic_action_delete_grey);
+            if (item.getLastResult() == UploadResult.SYNC_CONFLICT) {
+                itemViewHolder.button.setImageResource(R.drawable.ic_dots_vertical);
+                itemViewHolder.button.setOnClickListener(view -> {
+                    if (optionalUser.isPresent()) {
+                        Account account = optionalUser.get().toPlatformAccount();
+                        showItemConflictPopup(
+                            itemViewHolder, item, account, status, view
+                        );
+                    }
+                });
+            } else {
+                // Delete
+                itemViewHolder.button.setImageResource(R.drawable.ic_action_delete_grey);
+                itemViewHolder.button.setOnClickListener(v -> removeUpload(item));
+            }
             itemViewHolder.button.setVisibility(View.VISIBLE);
-            itemViewHolder.button.setOnClickListener(v -> {
-                uploadsStorageManager.removeUpload(item);
-                loadUploadItemsFromDb();
-            });
-
         } else {    // UploadStatus.UPLOAD_SUCCESS
             itemViewHolder.button.setVisibility(View.INVISIBLE);
         }
@@ -339,29 +360,32 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
 
         // click on item
         if (item.getUploadStatus() == UploadStatus.UPLOAD_FAILED) {
-            if (UploadResult.CREDENTIAL_ERROR == item.getLastResult()) {
-                itemViewHolder.itemLayout.setOnClickListener(v ->
-                                                                 parentActivity.getFileOperationsHelper().checkCurrentCredentials(
-                                                                     item.getAccount(accountManager)));
-            } else {
-                // not a credentials error
-                itemViewHolder.itemLayout.setOnClickListener(v -> {
-                    File file = new File(item.getLocalPath());
-                    if (file.exists()) {
-                        FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
-                        requester.retry(parentActivity, accountManager, item);
-                        loadUploadItemsFromDb();
-                    } else {
-                        DisplayUtils.showSnackMessage(
-                                v.getRootView().findViewById(android.R.id.content),
-                                R.string.local_file_not_found_message
-                        );
+            final UploadResult uploadResult = item.getLastResult();
+            itemViewHolder.itemLayout.setOnClickListener(v -> {
+                if (uploadResult == UploadResult.CREDENTIAL_ERROR) {
+                    parentActivity.getFileOperationsHelper().checkCurrentCredentials(item.getAccount(accountManager));
+                    return;
+                } else if (uploadResult == UploadResult.SYNC_CONFLICT && optionalUser.isPresent()) {
+                    Account account = optionalUser.get().toPlatformAccount();
+                    if (checkAndOpenConflictResolutionDialog(itemViewHolder, item, account, status)) {
+                        return;
                     }
-                });
-            }
+                }
+
+                // not a credentials error
+                File file = new File(item.getLocalPath());
+                if (file.exists()) {
+                    FileUploader.retryUpload(parentActivity, item.getAccount(accountManager), item);
+                    loadUploadItemsFromDb();
+                } else {
+                    DisplayUtils.showSnackMessage(
+                        v.getRootView().findViewById(android.R.id.content),
+                        R.string.local_file_not_found_message
+                    );
+                }
+            });
         } else {
-            itemViewHolder.itemLayout.setOnClickListener(v ->
-                    onUploadItemClick(item));
+            itemViewHolder.itemLayout.setOnClickListener(v -> onUploadItemClick(item));
         }
 
         // Set icon or thumbnail
@@ -466,6 +490,90 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter<SectionedVie
         }
     }
 
+    private boolean checkAndOpenConflictResolutionDialog(ItemViewHolder itemViewHolder, OCUpload item, Account account, String status) {
+        String remotePath = item.getRemotePath();
+        OCFile ocFile = storageManager.getFileByPath(remotePath);
+
+        if (ocFile == null) { // Remote file doesn't exist, try to refresh folder
+            OCFile folder = storageManager.getFileByPath(new File(remotePath).getParent() + "/");
+            if (folder != null && folder.isFolder()) {
+                this.refreshFolder(itemViewHolder, account, folder, (caller, result) -> {
+                    itemViewHolder.status.setText(status);
+                    if (result.isSuccess()) {
+                        OCFile file = storageManager.getFileByPath(remotePath);
+                        if (file != null) {
+                            this.openConflictActivity(file, item);
+                        }
+                    }
+                });
+                return true;
+            }
+
+            // Destination folder doesn't exist anymore
+        }
+
+        if (ocFile != null) {
+            this.openConflictActivity(ocFile, item);
+            return true;
+        }
+
+        // Remote file doesn't exist anymore = there is no more conflict
+        return false;
+    }
+
+    private void showItemConflictPopup(ItemViewHolder itemViewHolder, OCUpload item, Account account, String status, View view) {
+        PopupMenu popup = new PopupMenu(MainApp.getAppContext(), view);
+        popup.inflate(R.menu.upload_list_item_file_conflict);
+        popup.setOnMenuItemClickListener(i -> {
+            switch (i.getItemId()) {
+                case R.id.action_upload_list_resolve_conflict:
+                    checkAndOpenConflictResolutionDialog(itemViewHolder, item, account, status);
+                    break;
+                case R.id.action_upload_list_delete:
+                default:
+                    removeUpload(item);
+                    break;
+            }
+            return true;
+        });
+        popup.show();
+    }
+
+    private void removeUpload(OCUpload item) {
+        uploadsStorageManager.removeUpload(item);
+        loadUploadItemsFromDb();
+    }
+
+    private void refreshFolder(ItemViewHolder view, Account account, OCFile folder, OnRemoteOperationListener listener) {
+        view.itemLayout.setClickable(false);
+        view.status.setText(R.string.uploads_view_upload_status_fetching_server_version);
+        Context context = MainApp.getAppContext();
+        new RefreshFolderOperation(folder,
+                                   clock.getCurrentTime(),
+                                   false,
+                                   false,
+                                   true,
+                                   storageManager,
+                                   account,
+                                   context)
+            .execute(account, context, (caller, result) -> {
+                view.itemLayout.setClickable(true);
+                listener.onRemoteOperationFinish(caller, result);
+            }, parentActivity.getHandler());
+    }
+
+    private void openConflictActivity(OCFile file, OCUpload upload) {
+        file.setStoragePath(upload.getLocalPath());
+
+        Context context = MainApp.getAppContext();
+        Intent i = new Intent(context, ConflictsResolveActivity.class);
+        i.setFlags(i.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
+        i.putExtra(ConflictsResolveActivity.EXTRA_FILE, file);
+        i.putExtra(ConflictsResolveActivity.EXTRA_ACCOUNT, upload.getAccount(accountManager));
+        i.putExtra(ConflictsResolveActivity.EXTRA_CONFLICT_UPLOAD, upload);
+        context.startActivity(i);
+    }
+
     /**
      * Gets the status text to show to the user according to the status and last result of the
      * the given upload.

+ 3 - 3
src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java

@@ -219,8 +219,7 @@ public class CopyAndUploadContentUrisTask extends AsyncTask<Object, Void, Result
     }
 
     private void requestUpload(Account account, String localPath, String remotePath, int behaviour, String mimeType) {
-        FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
-        requester.uploadNewFile(
+        FileUploader.uploadNewFile(
             mAppContext,
             account,
             localPath,
@@ -230,7 +229,8 @@ public class CopyAndUploadContentUrisTask extends AsyncTask<Object, Void, Result
             false,      // do not create parent folder if not existent
             UploadFileOperation.CREATED_BY_USER,
             false,
-             false
+            false,
+            FileUploader.NameCollisionPolicy.ASK_USER
         );
     }
 

+ 36 - 32
src/main/java/com/owncloud/android/ui/dialog/ConflictsResolveDialog.java

@@ -43,44 +43,48 @@ public class ConflictsResolveDialog extends DialogFragment {
     public enum Decision {
         CANCEL,
         KEEP_BOTH,
-        OVERWRITE,
-        SERVER
+        KEEP_LOCAL,
+        KEEP_SERVER,
     }
 
-    OnConflictDecisionMadeListener mListener;
+    private final OnConflictDecisionMadeListener listener;
+    private final boolean canKeepServer;
 
-    public static ConflictsResolveDialog newInstance(OnConflictDecisionMadeListener listener) {
-        ConflictsResolveDialog f = new ConflictsResolveDialog();
-        f.mListener = listener;
-        return f;
+    public ConflictsResolveDialog(OnConflictDecisionMadeListener listener, boolean canKeepServer) {
+        this.listener = listener;
+        this.canKeepServer = canKeepServer;
     }
 
     @NonNull
     @Override
     public Dialog onCreateDialog(Bundle savedInstanceState) {
-        return new AlertDialog.Builder(requireActivity(), R.style.Theme_ownCloud_Dialog)
-                .setIcon(R.drawable.ic_warning)
-                .setTitle(R.string.conflict_title)
-                .setMessage(getString(R.string.conflict_message))
-                .setPositiveButton(R.string.conflict_use_local_version,
-                        (dialog, which) -> {
-                            if (mListener != null) {
-                                mListener.conflictDecisionMade(Decision.OVERWRITE);
-                            }
-                        })
-                .setNeutralButton(R.string.conflict_keep_both,
-                        (dialog, which) -> {
-                            if (mListener != null) {
-                                mListener.conflictDecisionMade(Decision.KEEP_BOTH);
-                            }
-                        })
-                .setNegativeButton(R.string.conflict_use_server_version,
-                        (dialog, which) -> {
-                            if (mListener != null) {
-                                mListener.conflictDecisionMade(Decision.SERVER);
-                            }
-                        })
-                .create();
+        AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity(), R.style.Theme_ownCloud_Dialog)
+            .setIcon(R.drawable.ic_warning)
+            .setTitle(R.string.conflict_title)
+            .setMessage(getString(R.string.conflict_message))
+            .setPositiveButton(R.string.conflict_use_local_version,
+                               (dialog, which) -> {
+                                   if (listener != null) {
+                                       listener.conflictDecisionMade(Decision.KEEP_LOCAL);
+                                   }
+                               })
+            .setNeutralButton(R.string.conflict_keep_both,
+                              (dialog, which) -> {
+                                  if (listener != null) {
+                                      listener.conflictDecisionMade(Decision.KEEP_BOTH);
+                                  }
+                              });
+
+        if (this.canKeepServer) {
+            builder.setNegativeButton(R.string.conflict_use_server_version,
+                                      (dialog, which) -> {
+                                          if (listener != null) {
+                                              listener.conflictDecisionMade(Decision.KEEP_SERVER);
+                                          }
+                                      });
+        }
+
+        return builder.create();
     }
 
     public void showDialog(AppCompatActivity activity) {
@@ -96,8 +100,8 @@ public class ConflictsResolveDialog extends DialogFragment {
 
     @Override
     public void onCancel(DialogInterface dialog) {
-        if (mListener != null) {
-            mListener.conflictDecisionMade(Decision.CANCEL);
+        if (listener != null) {
+            listener.conflictDecisionMade(Decision.CANCEL);
         }
     }
 

+ 17 - 0
src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.java

@@ -77,6 +77,7 @@ public class SyncedFolderPreferencesDialogFragment extends DialogFragment {
     private SwitchCompat mEnabledSwitch;
     private AppCompatCheckBox mUploadOnWifiCheckbox;
     private AppCompatCheckBox mUploadOnChargingCheckbox;
+    private AppCompatCheckBox mUploadExistingCheckbox;
     private AppCompatCheckBox mUploadUseSubfoldersCheckbox;
     private TextView mUploadBehaviorSummary;
     private TextView mLocalFolderPath;
@@ -189,6 +190,9 @@ public class SyncedFolderPreferencesDialogFragment extends DialogFragment {
             ThemeUtils.tintCheckbox(mUploadOnChargingCheckbox, accentColor);
         }
 
+        mUploadExistingCheckbox = view.findViewById(R.id.setting_instant_upload_existing_checkbox);
+        ThemeUtils.tintCheckbox(mUploadExistingCheckbox, accentColor);
+
         mUploadUseSubfoldersCheckbox = view.findViewById(
                 R.id.setting_instant_upload_path_use_subfolders_checkbox);
         ThemeUtils.tintCheckbox(mUploadUseSubfoldersCheckbox, accentColor);
@@ -227,6 +231,7 @@ public class SyncedFolderPreferencesDialogFragment extends DialogFragment {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
             mUploadOnChargingCheckbox.setChecked(mSyncedFolder.isChargingOnly());
         }
+        mUploadExistingCheckbox.setChecked(mSyncedFolder.isExisting());
         mUploadUseSubfoldersCheckbox.setChecked(mSyncedFolder.isSubfolderByDate());
 
         mUploadBehaviorSummary.setText(mUploadBehaviorItemStrings[mSyncedFolder.getUploadActionInteger()]);
@@ -318,6 +323,9 @@ public class SyncedFolderPreferencesDialogFragment extends DialogFragment {
             view.findViewById(R.id.setting_instant_upload_on_charging_container).setAlpha(alpha);
         }
 
+        view.findViewById(R.id.setting_instant_upload_existing_container).setEnabled(enable);
+        view.findViewById(R.id.setting_instant_upload_existing_container).setAlpha(alpha);
+
         view.findViewById(R.id.setting_instant_upload_path_use_subfolders_container).setEnabled(enable);
         view.findViewById(R.id.setting_instant_upload_path_use_subfolders_container).setAlpha(alpha);
 
@@ -361,6 +369,15 @@ public class SyncedFolderPreferencesDialogFragment extends DialogFragment {
                     });
         }
 
+        view.findViewById(R.id.setting_instant_upload_existing_container).setOnClickListener(
+              new OnClickListener() {
+                  @Override
+                  public void onClick(View v) {
+                      mSyncedFolder.setExisting(!mSyncedFolder.isExisting());
+                      mUploadExistingCheckbox.toggle();
+                  }
+              });
+
         view.findViewById(R.id.setting_instant_upload_path_use_subfolders_container).setOnClickListener(
                 new OnClickListener() {
                     @Override

+ 4 - 0
src/main/java/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelable.java

@@ -41,6 +41,7 @@ public class SyncedFolderParcelable implements Parcelable {
     @Getter @Setter private String remotePath;
     @Getter @Setter private boolean wifiOnly = false;
     @Getter @Setter private boolean chargingOnly = false;
+    @Getter @Setter private boolean existing = true;
     @Getter @Setter private boolean enabled = false;
     @Getter @Setter private boolean subfolderByDate = false;
     @Getter private Integer uploadAction;
@@ -57,6 +58,7 @@ public class SyncedFolderParcelable implements Parcelable {
         remotePath = syncedFolderDisplayItem.getRemotePath();
         wifiOnly = syncedFolderDisplayItem.isWifiOnly();
         chargingOnly = syncedFolderDisplayItem.isChargingOnly();
+        existing = syncedFolderDisplayItem.isExisting();
         enabled = syncedFolderDisplayItem.isEnabled();
         subfolderByDate = syncedFolderDisplayItem.isSubfolderByDate();
         type = syncedFolderDisplayItem.getType();
@@ -73,6 +75,7 @@ public class SyncedFolderParcelable implements Parcelable {
         remotePath = read.readString();
         wifiOnly = read.readInt()!= 0;
         chargingOnly = read.readInt() != 0;
+        existing = read.readInt() != 0;
         enabled = read.readInt() != 0;
         subfolderByDate = read.readInt() != 0;
         type = MediaFolderType.getById(read.readInt());
@@ -90,6 +93,7 @@ public class SyncedFolderParcelable implements Parcelable {
         dest.writeString(remotePath);
         dest.writeInt(wifiOnly ? 1 : 0);
         dest.writeInt(chargingOnly ? 1 : 0);
+        dest.writeInt(existing ? 1 : 0);
         dest.writeInt(enabled ? 1 : 0);
         dest.writeInt(subfolderByDate ? 1 : 0);
         dest.writeInt(type.getId());

+ 12 - 12
src/main/java/com/owncloud/android/ui/helpers/UriUploader.java

@@ -158,18 +158,18 @@ public class UriUploader {
      * @param remotePath    Absolute path in the current OC account to set to the uploaded file.
      */
     private void requestUpload(String localPath, String remotePath) {
-        FileUploader.UploadRequester requester = new FileUploader.UploadRequester();
-        requester.uploadNewFile(
-                mActivity,
-                mAccount,
-                localPath,
-                remotePath,
-                mBehaviour,
-                null,       // MIME type will be detected from file name
-                false,      // do not create parent folder if not existent
-                UploadFileOperation.CREATED_BY_USER,
-                false,
-                false
+        FileUploader.uploadNewFile(
+            mActivity,
+            mAccount,
+            localPath,
+            remotePath,
+            mBehaviour,
+            null,       // MIME type will be detected from file name
+            false,      // do not create parent folder if not existent
+            UploadFileOperation.CREATED_BY_USER,
+            false,
+            false,
+            FileUploader.NameCollisionPolicy.ASK_USER
         );
     }
 

+ 9 - 7
src/main/java/com/owncloud/android/utils/ErrorMessageAdapter.java

@@ -76,14 +76,14 @@ public final class ErrorMessageAdapter {
             RemoteOperation operation,
             Resources res
     ) {
-        String message = getSpecificMessageForResultAndOperation(result, operation, res);
+        String message = getMessageForResultAndOperation(result, operation, res);
 
         if (TextUtils.isEmpty(message)) {
-            message = getCommonMessageForResult(result, res);
+            message = getMessageForResult(result, res);
         }
 
         if (TextUtils.isEmpty(message)) {
-            message = getGenericErrorMessageForOperation(operation, res);
+            message = getMessageForOperation(operation, res);
         }
 
         if (message == null) {
@@ -109,7 +109,7 @@ public final class ErrorMessageAdapter {
      *                      specific message for both.
      */
     @Nullable
-    private static String getSpecificMessageForResultAndOperation(
+    private static String getMessageForResultAndOperation(
             RemoteOperationResult result,
             RemoteOperation operation,
             Resources res
@@ -360,6 +360,9 @@ public final class ErrorMessageAdapter {
             } else if (result.getCode() == ResultCode.INVALID_CHARACTER_DETECT_IN_SERVER) {
                 return res.getString(R.string.filename_forbidden_charaters_from_server);
 
+            } else if(result.getCode() == ResultCode.SYNC_CONFLICT) {
+                return String.format(res.getString(R.string.uploader_upload_failed_sync_conflict_error_content),
+                                        operation.getFileName());
             }
         }
 
@@ -376,8 +379,7 @@ public final class ErrorMessageAdapter {
      * @return              User message corresponding to 'result'.
      */
     @Nullable
-    private static String getCommonMessageForResult(RemoteOperationResult result, Resources res) {
-
+    private static String getMessageForResult(RemoteOperationResult result, Resources res) {
         String message = null;
 
         if (!result.isSuccess()) {
@@ -452,7 +454,7 @@ public final class ErrorMessageAdapter {
      * @return              User message corresponding to a generic error of 'operation'.
      */
     @Nullable
-    private static String getGenericErrorMessageForOperation(RemoteOperation operation, Resources res) {
+    private static String getMessageForOperation(RemoteOperation operation, Resources res) {
         String message = null;
 
         if (operation instanceof UploadFileOperation) {

+ 25 - 16
src/main/java/com/owncloud/android/utils/FilesSyncHelper.java

@@ -79,13 +79,13 @@ public final class FilesSyncHelper {
         // utility class -> private constructor
     }
 
-    public static void insertAllDBEntriesForSyncedFolder(SyncedFolder syncedFolder) {
+    public static void insertAllDBEntriesForSyncedFolder(SyncedFolder syncedFolder, boolean syncNow) {
         final Context context = MainApp.getAppContext();
         final ContentResolver contentResolver = context.getContentResolver();
 
         final long enabledTimestampMs = syncedFolder.getEnabledTimestampMs();
 
-        if (syncedFolder.isEnabled() && enabledTimestampMs >= 0) {
+        if (syncedFolder.isEnabled() && (syncedFolder.isExisting() || enabledTimestampMs >= 0)) {
             MediaFolderType mediaType = syncedFolder.getType();
             if (mediaType == MediaFolderType.IMAGE) {
                 FilesSyncHelper.insertContentIntoDB(MediaStore.Images.Media.INTERNAL_CONTENT_URI
@@ -106,7 +106,7 @@ public final class FilesSyncHelper {
                         @Override
                         public FileVisitResult visitFile(Path path, BasicFileAttributes attrs) {
                             File file = path.toFile();
-                            if (attrs.lastModifiedTime().toMillis() >= enabledTimestampMs) {
+                            if (syncedFolder.isExisting() || attrs.lastModifiedTime().toMillis() >= enabledTimestampMs) {
                                 filesystemDataProvider.storeOrUpdateFileValue(path.toAbsolutePath().toString(),
                                                                               attrs.lastModifiedTime().toMillis(),
                                                                               file.isDirectory(), syncedFolder);
@@ -124,17 +124,26 @@ public final class FilesSyncHelper {
                     Log_OC.e(TAG, "Something went wrong while indexing files for auto upload", e);
                 }
             }
+
+            if (syncNow) {
+                new JobRequest.Builder(FilesSyncJob.TAG)
+                    .setExact(1_000L)
+                    .setUpdateCurrent(false)
+                    .build()
+                    .schedule();
+            }
         }
     }
 
-    public static void insertAllDBEntries(AppPreferences preferences, Clock clock, boolean skipCustom) {
+    public static void insertAllDBEntries(AppPreferences preferences, Clock clock, boolean skipCustom,
+                                          boolean syncNow) {
         final Context context = MainApp.getAppContext();
         final ContentResolver contentResolver = context.getContentResolver();
         SyncedFolderProvider syncedFolderProvider = new SyncedFolderProvider(contentResolver, preferences, clock);
 
         for (SyncedFolder syncedFolder : syncedFolderProvider.getSyncedFolders()) {
-            if (syncedFolder.isEnabled() && (MediaFolderType.CUSTOM != syncedFolder.getType() || !skipCustom)) {
-                insertAllDBEntriesForSyncedFolder(syncedFolder);
+            if (syncedFolder.isEnabled() && (!skipCustom || syncedFolder.getType() != MediaFolderType.CUSTOM)) {
+                insertAllDBEntriesForSyncedFolder(syncedFolder, syncNow);
             }
         }
     }
@@ -171,7 +180,7 @@ public final class FilesSyncHelper {
             while (cursor.moveToNext()) {
                 contentPath = cursor.getString(column_index_data);
                 isFolder = new File(contentPath).isDirectory();
-                if (cursor.getLong(column_index_date_modified) >= enabledTimestampMs / 1000.0) {
+                if (syncedFolder.isExisting() || cursor.getLong(column_index_date_modified) >= enabledTimestampMs / 1000.0) {
                     filesystemDataProvider.storeOrUpdateFileValue(contentPath,
                                                                   cursor.getLong(column_index_date_modified), isFolder,
                                                                   syncedFolder);
@@ -187,8 +196,6 @@ public final class FilesSyncHelper {
                                            final PowerManagementService powerManagementService) {
         final Context context = MainApp.getAppContext();
 
-        FileUploader.UploadRequester uploadRequester = new FileUploader.UploadRequester();
-
         boolean accountExists;
 
         OCUpload[] failedUploads = uploadsStorageManager.getFailedUploads();
@@ -212,13 +219,15 @@ public final class FilesSyncHelper {
         new Thread(() -> {
             if (connectivityService.getActiveNetworkType() != JobRequest.NetworkType.ANY &&
                     !connectivityService.isInternetWalled()) {
-                uploadRequester.retryFailedUploads(context,
-                                                   null,
-                                                   uploadsStorageManager,
-                                                   connectivityService,
-                                                   accountManager,
-                                                   powerManagementService,
-                                                   null);
+                FileUploader.retryFailedUploads(
+                    context,
+                    null,
+                    uploadsStorageManager,
+                    connectivityService,
+                    accountManager,
+                    powerManagementService,
+                    null
+                );
             }
         }).start();
     }

+ 112 - 143
src/main/res/layout/synced_folders_settings_layout.xml

@@ -19,12 +19,11 @@
   License along with this program.  If not, see <http://www.gnu.org/licenses/>.
 -->
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
-              android:id="@+id/root"
-              android:layout_width="wrap_content"
-              android:layout_height="wrap_content"
-              android:gravity="center"
-              android:orientation="vertical"
-              android:padding="@dimen/standard_padding">
+    android:id="@+id/root"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:gravity="center"
+    android:orientation="vertical">
 
     <LinearLayout
         android:id="@+id/top_title"
@@ -37,15 +36,14 @@
             android:layout_height="wrap_content"
             android:layout_weight="1"
             android:orientation="vertical"
-            android:paddingBottom="@dimen/standard_padding"
-            android:paddingTop="@dimen/standard_padding">
+            android:padding="@dimen/standard_padding">
 
             <TextView
                 android:id="@+id/synced_folders_settings_title"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
                 android:text="@string/synced_folders_preferences"
-                android:textAppearance="@style/TextAppearance.AppCompat.Title"/>
+                android:textAppearance="@style/TextAppearance.AppCompat.Title" />
 
             <TextView
                 android:id="@+id/synced_folders_settings_local_folder_path"
@@ -53,20 +51,15 @@
                 android:layout_height="wrap_content"
                 android:ellipsize="middle"
                 android:maxLines="2"
-                android:textColor="?android:attr/textColorSecondary"/>
-
+                android:textColor="?android:attr/textColorSecondary" />
         </LinearLayout>
 
         <LinearLayout
-            android:layout_width="wrap_content"
+            android:id="@+id/synced_folders_enable_switch_container"
+            android:layout_width="@dimen/synced_folders_control_width"
             android:layout_height="match_parent"
-            android:gravity="end|top"
-            android:orientation="vertical"
-            android:paddingLeft="@dimen/standard_padding"
-            android:paddingStart="@dimen/standard_padding"
-            android:paddingRight="@dimen/zero"
-            android:paddingEnd="@dimen/zero"
-            android:paddingTop="@dimen/standard_padding">
+            android:gravity="center"
+            android:padding="@dimen/standard_padding">
 
             <androidx.appcompat.widget.SwitchCompat
                 android:id="@+id/sync_enabled"
@@ -74,10 +67,8 @@
                 android:layout_height="wrap_content"
                 android:background="@null"
                 android:clickable="false"
-                android:focusable="false"/>
-
+                android:focusable="false" />
         </LinearLayout>
-
     </LinearLayout>
 
     <ScrollView
@@ -95,17 +86,13 @@
                 android:id="@+id/local_folder_container"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:baselineAligned="false"
-                android:clipToPadding="false"
-                android:gravity="center_vertical"
-                android:minHeight="?attr/listPreferredItemHeightSmall">
+                android:baselineAligned="false">
 
                 <RelativeLayout
                     android:layout_width="0dp"
                     android:layout_height="wrap_content"
                     android:layout_weight="1"
-                    android:paddingBottom="@dimen/standard_padding"
-                    android:paddingTop="@dimen/standard_padding">
+                    android:padding="@dimen/standard_padding">
 
                     <TextView
                         android:id="@+id/local_folder_title"
@@ -113,62 +100,51 @@
                         android:layout_height="wrap_content"
                         android:maxLines="2"
                         android:text="@string/prefs_synced_folders_local_path_title"
-                        android:textAppearance="?attr/textAppearanceListItem"/>
+                        android:textAppearance="?attr/textAppearanceListItem" />
 
                     <TextView
                         android:id="@+id/local_folder_summary"
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
-                        android:layout_alignLeft="@id/local_folder_title"
-                        android:layout_alignStart="@id/local_folder_title"
                         android:layout_below="@id/local_folder_title"
+                        android:layout_alignStart="@id/local_folder_title"
+                        android:layout_alignLeft="@id/local_folder_title"
                         android:ellipsize="middle"
                         android:maxLines="2"
                         android:text="@string/choose_local_folder"
-                        android:textColor="?android:attr/textColorSecondary"/>
-
+                        android:textColor="?android:attr/textColorSecondary" />
                 </RelativeLayout>
 
                 <!-- Preference should place its actual preference widget here. -->
                 <LinearLayout
                     android:id="@+id/local_folder_frame"
-                    android:layout_width="wrap_content"
+                    android:layout_width="@dimen/synced_folders_control_width"
                     android:layout_height="match_parent"
-                    android:gravity="end|center_vertical"
-                    android:orientation="vertical"
-                    android:paddingLeft="@dimen/standard_padding"
-                    android:paddingStart="@dimen/standard_padding"
-                    android:paddingRight="@dimen/zero"
-                    android:paddingEnd="@dimen/zero">
+                    android:gravity="center"
+                    android:padding="@dimen/standard_padding">
 
                     <ImageView
                         android:id="@+id/local_folder_icon"
                         android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
+                        android:layout_height="fill_parent"
                         android:background="@null"
+                        android:contentDescription="@string/folder_icon"
                         android:padding="@dimen/standard_quarter_padding"
-                        android:src="@drawable/ic_folder_open"
-                        android:contentDescription="@string/folder_icon"/>
-
+                        android:src="@drawable/ic_folder_open" />
                 </LinearLayout>
-
             </LinearLayout>
 
             <LinearLayout
                 android:id="@+id/remote_folder_container"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:baselineAligned="false"
-                android:clipToPadding="false"
-                android:gravity="center_vertical"
-                android:minHeight="?attr/listPreferredItemHeightSmall">
+                android:baselineAligned="false">
 
                 <RelativeLayout
                     android:layout_width="0dp"
                     android:layout_height="wrap_content"
                     android:layout_weight="1"
-                    android:paddingBottom="@dimen/standard_padding"
-                    android:paddingTop="@dimen/standard_padding">
+                    android:padding="@dimen/standard_padding">
 
                     <TextView
                         android:id="@+id/remote_folder_title"
@@ -176,62 +152,51 @@
                         android:layout_height="wrap_content"
                         android:maxLines="2"
                         android:text="@string/prefs_synced_folders_remote_path_title"
-                        android:textAppearance="?attr/textAppearanceListItem"/>
+                        android:textAppearance="?attr/textAppearanceListItem" />
 
                     <TextView
                         android:id="@+id/remote_folder_summary"
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
+                        android:layout_below="@id/remote_folder_title"
                         android:layout_alignStart="@id/remote_folder_title"
                         android:layout_alignLeft="@id/remote_folder_title"
-                        android:layout_below="@id/remote_folder_title"
                         android:ellipsize="middle"
                         android:maxLines="2"
                         android:text="@string/choose_remote_folder"
-                        android:textColor="?android:attr/textColorSecondary"/>
-
+                        android:textColor="?android:attr/textColorSecondary" />
                 </RelativeLayout>
 
                 <!-- Preference should place its actual preference widget here. -->
                 <LinearLayout
                     android:id="@+id/remote_folder_frame"
-                    android:layout_width="wrap_content"
+                    android:layout_width="@dimen/synced_folders_control_width"
                     android:layout_height="match_parent"
-                    android:gravity="end|center_vertical"
-                    android:orientation="vertical"
-                    android:paddingLeft="@dimen/standard_padding"
-                    android:paddingStart="@dimen/standard_padding"
-                    android:paddingEnd="@dimen/zero"
-                    android:paddingRight="@dimen/zero">
+                    android:gravity="center"
+                    android:padding="@dimen/standard_padding">
 
                     <ImageView
                         android:id="@+id/remote_folder_icon"
                         android:layout_width="wrap_content"
-                        android:layout_height="wrap_content"
+                        android:layout_height="fill_parent"
                         android:background="@null"
+                        android:contentDescription="@string/folder_icon"
                         android:padding="@dimen/standard_quarter_padding"
-                        android:src="@drawable/ic_folder_open"
-                        android:contentDescription="@string/folder_icon"/>
-
+                        android:src="@drawable/ic_folder_open" />
                 </LinearLayout>
-
             </LinearLayout>
 
             <LinearLayout
                 android:id="@+id/setting_instant_upload_on_wifi_container"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:baselineAligned="false"
-                android:clipToPadding="false"
-                android:gravity="center_vertical"
-                android:minHeight="?attr/listPreferredItemHeightSmall">
+                android:baselineAligned="false">
 
                 <RelativeLayout
                     android:layout_width="0dp"
                     android:layout_height="wrap_content"
                     android:layout_weight="1"
-                    android:paddingBottom="@dimen/standard_padding"
-                    android:paddingTop="@dimen/standard_padding">
+                    android:padding="@dimen/standard_padding">
 
                     <TextView
                         android:id="@+id/setting_instant_upload_on_wifi_label"
@@ -240,48 +205,37 @@
                         android:ellipsize="marquee"
                         android:maxLines="2"
                         android:text="@string/auto_upload_on_wifi"
-                        android:textAppearance="?attr/textAppearanceListItem"/>
-
+                        android:textAppearance="?attr/textAppearanceListItem" />
                 </RelativeLayout>
 
                 <LinearLayout
                     android:id="@+id/setting_instant_upload_on_wifi_frame"
-                    android:layout_width="wrap_content"
+                    android:layout_width="@dimen/synced_folders_control_width"
                     android:layout_height="match_parent"
-                    android:gravity="end|center_vertical"
-                    android:orientation="vertical"
-                    android:paddingLeft="@dimen/standard_padding"
-                    android:paddingStart="@dimen/standard_padding"
-                    android:paddingRight="@dimen/zero"
-                    android:paddingEnd="@dimen/zero">
+                    android:gravity="center"
+                    android:padding="@dimen/standard_padding">
 
                     <androidx.appcompat.widget.AppCompatCheckBox
                         android:id="@+id/setting_instant_upload_on_wifi_checkbox"
-                        android:layout_width="wrap_content"
+                        android:layout_width="32dp"
                         android:layout_height="wrap_content"
                         android:background="@null"
                         android:clickable="false"
-                        android:focusable="false"/>
-
+                        android:focusable="false" />
                 </LinearLayout>
-
             </LinearLayout>
 
             <LinearLayout
                 android:id="@+id/setting_instant_upload_on_charging_container"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:baselineAligned="false"
-                android:clipToPadding="false"
-                android:gravity="center_vertical"
-                android:minHeight="?attr/listPreferredItemHeightSmall">
+                android:baselineAligned="false">
 
                 <RelativeLayout
                     android:layout_width="0dp"
                     android:layout_height="wrap_content"
                     android:layout_weight="1"
-                    android:paddingBottom="@dimen/standard_padding"
-                    android:paddingTop="@dimen/standard_padding">
+                    android:padding="@dimen/standard_padding">
 
                     <TextView
                         android:id="@+id/setting_instant_upload_on_charging_label"
@@ -290,48 +244,76 @@
                         android:ellipsize="marquee"
                         android:maxLines="2"
                         android:text="@string/instant_upload_on_charging"
-                        android:textAppearance="?attr/textAppearanceListItem"/>
-
+                        android:textAppearance="?attr/textAppearanceListItem" />
                 </RelativeLayout>
 
                 <LinearLayout
                     android:id="@+id/setting_instant_upload_on_charging_frame"
-                    android:layout_width="wrap_content"
+                    android:layout_width="@dimen/synced_folders_control_width"
                     android:layout_height="match_parent"
-                    android:gravity="end|center_vertical"
-                    android:orientation="vertical"
-                    android:paddingLeft="@dimen/standard_padding"
-                    android:paddingStart="@dimen/standard_padding"
-                    android:paddingRight="@dimen/zero"
-                    android:paddingEnd="@dimen/zero">
+                    android:gravity="center"
+                    android:padding="@dimen/standard_padding">
 
                     <androidx.appcompat.widget.AppCompatCheckBox
                         android:id="@+id/setting_instant_upload_on_charging_checkbox"
-                        android:layout_width="wrap_content"
+                        android:layout_width="32dp"
                         android:layout_height="wrap_content"
                         android:background="@null"
                         android:clickable="false"
-                        android:focusable="false"/>
-
+                        android:focusable="false" />
                 </LinearLayout>
+            </LinearLayout>
 
+            <LinearLayout
+                android:id="@+id/setting_instant_upload_existing_container"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:baselineAligned="false">
+
+                <RelativeLayout
+                    android:layout_width="0dp"
+                    android:layout_height="wrap_content"
+                    android:layout_weight="1"
+                    android:padding="@dimen/standard_padding">
+
+                    <TextView
+                        android:id="@+id/setting_instant_upload_existing_label"
+                        android:layout_width="wrap_content"
+                        android:layout_height="wrap_content"
+                        android:ellipsize="marquee"
+                        android:singleLine="true"
+                        android:text="@string/instant_upload_existing"
+                        android:textAppearance="?attr/textAppearanceListItem" />
+                </RelativeLayout>
+
+                <LinearLayout
+                    android:id="@+id/setting_instant_upload_existing"
+                    android:layout_width="@dimen/synced_folders_control_width"
+                    android:layout_height="match_parent"
+                    android:gravity="center"
+                    android:padding="@dimen/standard_padding">
+
+                    <androidx.appcompat.widget.AppCompatCheckBox
+                        android:id="@+id/setting_instant_upload_existing_checkbox"
+                        android:layout_width="32dp"
+                        android:layout_height="wrap_content"
+                        android:background="@null"
+                        android:clickable="false"
+                        android:focusable="false" />
+                </LinearLayout>
             </LinearLayout>
 
             <LinearLayout
                 android:id="@+id/setting_instant_upload_path_use_subfolders_container"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:baselineAligned="false"
-                android:clipToPadding="false"
-                android:gravity="center_vertical"
-                android:minHeight="?attr/listPreferredItemHeightSmall">
+                android:baselineAligned="false">
 
                 <RelativeLayout
                     android:layout_width="0dp"
                     android:layout_height="wrap_content"
                     android:layout_weight="1"
-                    android:paddingBottom="@dimen/standard_padding"
-                    android:paddingTop="@dimen/standard_padding">
+                    android:padding="@dimen/standard_padding">
 
                     <TextView
                         android:id="@+id/setting_instant_upload_path_use_subfolders_label"
@@ -340,44 +322,37 @@
                         android:ellipsize="marquee"
                         android:maxLines="2"
                         android:text="@string/prefs_instant_upload_path_use_subfolders_title"
-                        android:textAppearance="?attr/textAppearanceListItem"/>
+                        android:textAppearance="?attr/textAppearanceListItem" />
 
                     <TextView
                         android:id="@+id/setting_instant_upload_path_use_subfolders_summary"
                         android:layout_width="wrap_content"
                         android:layout_height="wrap_content"
-                        android:layout_alignLeft="@id/setting_instant_upload_path_use_subfolders_label"
-                        android:layout_alignStart="@id/setting_instant_upload_path_use_subfolders_label"
                         android:layout_below="@id/setting_instant_upload_path_use_subfolders_label"
+                        android:layout_alignStart="@id/setting_instant_upload_path_use_subfolders_label"
+                        android:layout_alignLeft="@id/setting_instant_upload_path_use_subfolders_label"
                         android:ellipsize="end"
                         android:maxLines="2"
                         android:text="@string/prefs_instant_upload_path_use_subfolders_summary"
-                        android:textColor="?android:attr/textColorSecondary"/>
-
+                        android:textColor="?android:attr/textColorSecondary" />
                 </RelativeLayout>
 
                 <!-- Preference should place its actual preference widget here. -->
                 <LinearLayout
                     android:id="@+id/setting_instant_upload_path_use_subfolders_frame"
-                    android:layout_width="wrap_content"
+                    android:layout_width="@dimen/synced_folders_control_width"
                     android:layout_height="match_parent"
-                    android:gravity="end|center_vertical"
-                    android:orientation="vertical"
-                    android:paddingLeft="@dimen/standard_padding"
-                    android:paddingStart="@dimen/standard_padding"
-                    android:paddingRight="@dimen/zero"
-                    android:paddingEnd="@dimen/zero">
+                    android:gravity="center"
+                    android:padding="@dimen/standard_padding">
 
                     <androidx.appcompat.widget.AppCompatCheckBox
                         android:id="@+id/setting_instant_upload_path_use_subfolders_checkbox"
-                        android:layout_width="wrap_content"
+                        android:layout_width="32dp"
                         android:layout_height="wrap_content"
                         android:background="@null"
                         android:clickable="false"
-                        android:focusable="false"/>
-
+                        android:focusable="false" />
                 </LinearLayout>
-
             </LinearLayout>
 
             <LinearLayout
@@ -389,8 +364,7 @@
                 android:gravity="center_vertical"
                 android:minHeight="?attr/listPreferredItemHeightSmall"
                 android:orientation="vertical"
-                android:paddingBottom="@dimen/standard_padding"
-                android:paddingTop="@dimen/standard_padding">
+                android:padding="@dimen/standard_padding">
 
                 <TextView
                     android:id="@+id/setting_instant_behaviour_title"
@@ -399,7 +373,7 @@
                     android:ellipsize="marquee"
                     android:maxLines="2"
                     android:text="@string/prefs_instant_behaviour_title"
-                    android:textAppearance="?attr/textAppearanceListItem"/>
+                    android:textAppearance="?attr/textAppearanceListItem" />
 
                 <TextView
                     android:id="@+id/setting_instant_behaviour_summary"
@@ -408,49 +382,44 @@
                     android:ellipsize="end"
                     android:maxLines="2"
                     android:text="@string/placeholder_filename"
-                    android:textColor="?android:attr/textColorSecondary"/>
-
+                    android:textColor="?android:attr/textColorSecondary" />
             </LinearLayout>
-
         </LinearLayout>
-
     </ScrollView>
 
     <RelativeLayout
         android:layout_width="match_parent"
-        android:layout_height="wrap_content">
+        android:layout_height="wrap_content"
+        android:padding="@dimen/standard_padding">
 
         <com.google.android.material.button.MaterialButton
             android:id="@+id/delete"
             style="@style/Button.Borderless.Destructive"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_alignParentLeft="true"
             android:layout_alignParentStart="true"
-            android:text="@string/common_delete"/>
+            android:layout_alignParentLeft="true"
+            android:text="@string/common_delete" />
 
         <LinearLayout
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
-            android:layout_alignParentRight="true"
-            android:layout_alignParentEnd="true">
+            android:layout_alignParentEnd="true"
+            android:layout_alignParentRight="true">
 
             <com.google.android.material.button.MaterialButton
                 android:id="@+id/cancel"
                 style="@style/Button.Borderless"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:text="@string/common_cancel"/>
+                android:text="@string/common_cancel" />
 
             <com.google.android.material.button.MaterialButton
                 android:id="@+id/save"
                 style="@style/Button.Borderless"
                 android:layout_width="wrap_content"
                 android:layout_height="wrap_content"
-                android:text="@string/common_save"/>
-
+                android:text="@string/common_save" />
         </LinearLayout>
-
     </RelativeLayout>
-
 </LinearLayout>

+ 30 - 0
src/main/res/menu/upload_list_item_file_conflict.xml

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Nextcloud Android client application
+
+  Copyright (C) 2019 Nextcloud GmbH.
+
+  This program is free software: you can redistribute it and/or modify
+  it under the terms of the GNU 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 General Public License for more details.
+
+  You should have received a copy of the GNU 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_resolve_conflict"
+        android:icon="@drawable/ic_history"
+        android:title="@string/upload_list_resolve_conflict" />
+
+    <item
+        android:id="@+id/action_upload_list_delete"
+        android:title="@string/upload_list_delete"
+        android:icon="@drawable/nav_trashbin" />
+</menu>

+ 1 - 0
src/main/res/values/dims.xml

@@ -135,6 +135,7 @@
     <dimen name="synced_folders_item_type_layout_height">32dp</dimen>
     <dimen name="synced_folders_item_type_layout_right_end_margin">24dp</dimen>
     <dimen name="synced_folders_recycler_view_layout_margin">-3dp</dimen>
+    <dimen name="synced_folders_control_width">80dp</dimen>
     <dimen name="toolbar_user_information_layout_margin">12dp</dimen>
     <dimen name="bottom_sheet_text_size">16sp</dimen>
     <dimen name="permission_dialog_text_size">18sp</dimen>

+ 6 - 0
src/main/res/values/strings.xml

@@ -171,6 +171,7 @@
     <string name="uploads_view_upload_status_unknown_fail">Unknown error</string>
     <string name="uploads_view_upload_status_waiting_for_wifi">Waiting for Wi-Fi</string>
     <string name="uploads_view_upload_status_waiting_exit_power_save_mode">Waiting to exit power save mode</string>
+    <string name="uploads_view_upload_status_fetching_server_version">Fetching server version…</string>
     <string name="uploads_view_later_waiting_to_upload">Waiting to upload</string>
     <string name="uploads_view_group_header" translatable="false">%1$s (%2$d)</string>
     <string name="downloader_download_in_progress_ticker">Downloading…</string>
@@ -323,6 +324,7 @@
 
     <string name="auto_upload_on_wifi">Only upload on unmetered Wi-Fi</string>
     <string name="instant_upload_on_charging">Only upload when charging</string>
+    <string name="instant_upload_existing">Also upload existing files</string>
     <string name="instant_upload_path">/InstantUpload</string>
     <string name="auto_upload_path">/AutoUpload</string>
     <string name="conflict_title">File conflict</string>
@@ -912,6 +914,10 @@
     <string name="create_rich_workspace">Add folder info</string>
     <string name="creates_rich_workspace">creates folder info</string>
     <string name="edit_rich_workspace">edit folder info</string>
+    <string name="uploader_upload_failed_sync_conflict_error">File upload conflict</string>
+    <string name="uploader_upload_failed_sync_conflict_error_content">Pick which version to keep of %1$s</string>
+    <string name="upload_list_resolve_conflict">Resolve conflict</string>
+    <string name="upload_list_delete">Delete</string>
     <string name="create_new">Create new</string>
     <string name="editor_placeholder" translatable="false">%1$s %2$s</string>
 

+ 1 - 0
src/test/java/com/owncloud/android/ui/activity/SyncedFoldersActivityTest.java

@@ -164,6 +164,7 @@ public class SyncedFoldersActivityTest {
                                            true,
                                            true,
                                            true,
+                                           true,
                                            "test@nextcloud.com",
                                            1,
                                            enabled,

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels