Browse Source

Merge pull request #5491 from nextcloud/conflictDialog

Enhance conflict dialog
Tobias Kaminsky 5 years ago
parent
commit
c00b13df12
21 changed files with 1105 additions and 119 deletions
  1. 4 0
      .idea/codeStyles/Project.xml
  2. BIN
      screenshots/com.nextcloud.client.ConflictsResolveActivityIT_test.png
  3. BIN
      screenshots/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth.png
  4. BIN
      screenshots/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting.png
  5. BIN
      screenshots/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew.png
  6. BIN
      screenshots/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotImages.png
  7. BIN
      screenshots/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles.png
  8. BIN
      screenshots/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_test.png
  9. BIN
      screenshots/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_test2.png
  10. 1 1
      scripts/updateScreenshots.sh
  11. BIN
      src/androidTest/assets/image.jpg
  12. 383 0
      src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt
  13. 290 0
      src/androidTest/java/com/owncloud/android/ui/activity/ConflictsResolveActivityIT.java
  14. 2 0
      src/main/java/com/owncloud/android/datamodel/OCFile.java
  15. 9 3
      src/main/java/com/owncloud/android/operations/UploadFileOperation.java
  16. 98 61
      src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.java
  17. 9 8
      src/main/java/com/owncloud/android/ui/adapter/LocalFileListAdapter.java
  18. 21 8
      src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java
  19. 172 32
      src/main/java/com/owncloud/android/ui/dialog/ConflictsResolveDialog.java
  20. 110 0
      src/main/res/layout/conflict_resolve_dialog.xml
  21. 6 6
      src/main/res/values/strings.xml

+ 4 - 0
.idea/codeStyles/Project.xml

@@ -73,6 +73,7 @@
     </XML>
     <codeStyleSettings language="JAVA">
       <option name="ALIGN_MULTILINE_PARAMETERS_IN_CALLS" value="true" />
+      <option name="ALIGN_MULTILINE_METHOD_BRACKETS" value="true" />
       <option name="WRAP_COMMENTS" value="true" />
       <option name="IF_BRACE_FORCE" value="3" />
       <option name="DOWHILE_BRACE_FORCE" value="3" />
@@ -192,5 +193,8 @@
         </rules>
       </arrangement>
     </codeStyleSettings>
+    <codeStyleSettings language="kotlin">
+      <option name="ALIGN_MULTILINE_METHOD_BRACKETS" value="true" />
+    </codeStyleSettings>
   </code_scheme>
 </component>

BIN
screenshots/com.nextcloud.client.ConflictsResolveActivityIT_test.png


BIN
screenshots/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepBoth.png


BIN
screenshots/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepExisting.png


BIN
screenshots/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_keepNew.png


BIN
screenshots/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotImages.png


BIN
screenshots/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_screenshotTextFiles.png


BIN
screenshots/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_test.png


BIN
screenshots/com.owncloud.android.ui.activity.ConflictsResolveActivityIT_test2.png


+ 1 - 1
scripts/updateScreenshots.sh

@@ -6,7 +6,7 @@ if ( [[ $(grep NC_TEST_SERVER_BASEURL ~/.gradle/gradle.properties   | grep -v "#
 fi
 
 ## emulator
-if ( [[ $(emulator -list-avds | grep uiComparison -c) -eq 0 ]] ); then
+if ( [[ ! $(emulator -list-avds | grep uiComparison -c) -eq 0 ]] ); then
     avdmanager delete avd -n uiComparison
     (sleep 5; echo "no") | avdmanager create avd -n uiComparison -c 100M -k "system-images;android-27;google_apis;x86" --abi "google_apis/x86"
 fi

BIN
src/androidTest/assets/image.jpg


+ 383 - 0
src/androidTest/java/com/owncloud/android/files/services/FileUploaderIT.kt

@@ -0,0 +1,383 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2020 Tobias Kaminsky
+ * Copyright (C) 2020 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+package com.owncloud.android.files.services
+
+import com.evernote.android.job.JobRequest
+import com.nextcloud.client.account.UserAccountManager
+import com.nextcloud.client.account.UserAccountManagerImpl
+import com.nextcloud.client.device.PowerManagementService
+import com.nextcloud.client.network.ConnectivityService
+import com.owncloud.android.AbstractIT
+import com.owncloud.android.datamodel.OCFile
+import com.owncloud.android.datamodel.UploadsStorageManager
+import com.owncloud.android.db.OCUpload
+import com.owncloud.android.lib.resources.files.ReadFileRemoteOperation
+import com.owncloud.android.lib.resources.files.model.RemoteFile
+import com.owncloud.android.operations.UploadFileOperation
+import com.owncloud.android.utils.FileStorageUtils.getSavePath
+import junit.framework.Assert.assertEquals
+import junit.framework.Assert.assertFalse
+import junit.framework.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import java.io.File
+
+class FileUploaderIT : AbstractIT() {
+    val SHORT_WAIT: Long = 5000
+    val LONG_WAIT: Long = 20000
+
+    var uploadsStorageManager: UploadsStorageManager? = null
+
+    val connectivityServiceMock: ConnectivityService = object : ConnectivityService {
+        override fun isInternetWalled(): Boolean {
+            return false
+        }
+
+        override fun isOnlineWithWifi(): Boolean {
+            return true
+        }
+
+        override fun getActiveNetworkType(): JobRequest.NetworkType {
+            return JobRequest.NetworkType.ANY
+        }
+    }
+
+    private val powerManagementServiceMock: PowerManagementService = object : PowerManagementService {
+        override val isPowerSavingEnabled: Boolean
+            get() = false
+
+        override val isPowerSavingExclusionAvailable: Boolean
+            get() = false
+
+        override val isBatteryCharging: Boolean
+            get() = false
+    }
+
+    @Before
+    fun setUp() {
+        val contentResolver = targetContext.contentResolver
+        val accountManager: UserAccountManager = UserAccountManagerImpl.fromContext(targetContext)
+        uploadsStorageManager = UploadsStorageManager(accountManager, contentResolver)
+    }
+
+    /**
+     * uploads a file, overwrites it with an empty one, check if overwritten
+     */
+    @Test
+    fun testKeepLocalAndOverwriteRemote() {
+        val file = File(getSavePath(account.name) + "/chunkedFile.txt")
+        val ocUpload = OCUpload(file.absolutePath, "/testFile.txt", account.name)
+
+        assertTrue(UploadFileOperation(
+            uploadsStorageManager,
+            connectivityServiceMock,
+            powerManagementServiceMock,
+            account,
+            null,
+            ocUpload,
+            FileUploader.NameCollisionPolicy.DEFAULT,
+            FileUploader.LOCAL_BEHAVIOUR_COPY,
+            targetContext,
+            false,
+            false)
+            .setRemoteFolderToBeCreated()
+            .execute(client, storageManager).isSuccess)
+
+        val result = ReadFileRemoteOperation("/testFile.txt").execute(client)
+        assertTrue(result.isSuccess)
+
+        assertEquals(file.length(), (result.data[0] as RemoteFile).length)
+
+        val ocUpload2 = OCUpload(getSavePath(account.name) + "/empty.txt", "/testFile.txt", account.name)
+
+        assertTrue(UploadFileOperation(
+            uploadsStorageManager,
+            connectivityServiceMock,
+            powerManagementServiceMock,
+            account,
+            null,
+            ocUpload2,
+            FileUploader.NameCollisionPolicy.OVERWRITE,
+            FileUploader.LOCAL_BEHAVIOUR_COPY,
+            targetContext,
+            false,
+            false)
+            .execute(client, storageManager).isSuccess)
+
+        val result2 = ReadFileRemoteOperation("/testFile.txt").execute(client)
+        assertTrue(result2.isSuccess)
+
+        assertEquals(0, (result2.data[0] as RemoteFile).length)
+    }
+
+    /**
+     * uploads a file, overwrites it with an empty one, check if overwritten
+     */
+    @Test
+    fun testKeepLocalAndOverwriteRemoteStatic() {
+        val file = File(getSavePath(account.name) + "/chunkedFile.txt")
+
+        FileUploader.uploadNewFile(
+            targetContext,
+            account,
+            file.absolutePath,
+            "/testFile.txt",
+            FileUploader.LOCAL_BEHAVIOUR_COPY,
+            null,
+            true,
+            UploadFileOperation.CREATED_BY_USER,
+            false,
+            false,
+            FileUploader.NameCollisionPolicy.DEFAULT)
+
+        Thread.sleep(LONG_WAIT)
+
+        val result = ReadFileRemoteOperation("/testFile.txt").execute(client)
+        assertTrue(result.isSuccess)
+
+        assertEquals(file.length(), (result.data[0] as RemoteFile).length)
+
+        val ocFile2 = OCFile("/testFile.txt")
+        ocFile2.setStoragePath(getSavePath(account.name) + "/empty.txt")
+
+        FileUploader.uploadUpdateFile(
+            targetContext,
+            account,
+            ocFile2,
+            FileUploader.LOCAL_BEHAVIOUR_COPY,
+            FileUploader.NameCollisionPolicy.OVERWRITE)
+
+        Thread.sleep(SHORT_WAIT)
+
+        val result2 = ReadFileRemoteOperation("/testFile.txt").execute(client)
+        assertTrue(result2.isSuccess)
+
+        assertEquals(0, (result2.data[0] as RemoteFile).length)
+    }
+
+    /**
+     * uploads a file, uploads another one with automatically (2) added, check
+     */
+    @Test
+    fun testKeepBoth() {
+        var renameListenerWasTriggered = false
+
+        val file = File(getSavePath(account.name) + "/chunkedFile.txt")
+        val ocUpload = OCUpload(file.absolutePath, "/testFile.txt", account.name)
+
+        assertTrue(UploadFileOperation(
+            uploadsStorageManager,
+            connectivityServiceMock,
+            powerManagementServiceMock,
+            account,
+            null,
+            ocUpload,
+            FileUploader.NameCollisionPolicy.DEFAULT,
+            FileUploader.LOCAL_BEHAVIOUR_COPY,
+            targetContext,
+            false,
+            false)
+            .setRemoteFolderToBeCreated()
+            .execute(client, storageManager).isSuccess)
+
+        val result = ReadFileRemoteOperation("/testFile.txt").execute(client)
+        assertTrue(result.isSuccess)
+
+        assertEquals(file.length(), (result.data[0] as RemoteFile).length)
+
+        val file2 = File(getSavePath(account.name) + "/empty.txt")
+        val ocUpload2 = OCUpload(file2.absolutePath, "/testFile.txt", account.name)
+
+        assertTrue(UploadFileOperation(
+            uploadsStorageManager,
+            connectivityServiceMock,
+            powerManagementServiceMock,
+            account,
+            null,
+            ocUpload2,
+            FileUploader.NameCollisionPolicy.RENAME,
+            FileUploader.LOCAL_BEHAVIOUR_COPY,
+            targetContext,
+            false,
+            false)
+            .addRenameUploadListener {
+                renameListenerWasTriggered = true
+            }
+            .execute(client, storageManager).isSuccess)
+
+        val result2 = ReadFileRemoteOperation("/testFile.txt").execute(client)
+        assertTrue(result2.isSuccess)
+
+        assertEquals(file.length(), (result2.data[0] as RemoteFile).length)
+
+        val result3 = ReadFileRemoteOperation("/testFile (2).txt").execute(client)
+        assertTrue(result3.isSuccess)
+
+        assertEquals(file2.length(), (result3.data[0] as RemoteFile).length)
+        assertTrue(renameListenerWasTriggered)
+    }
+
+    /**
+     * uploads a file, uploads another one with automatically (2) added, check
+     */
+    @Test
+    fun testKeepBothStatic() {
+        val file = File(getSavePath(account.name) + "/chunkedFile.txt")
+
+        FileUploader.uploadNewFile(
+            targetContext,
+            account,
+            file.absolutePath,
+            "/testFile.txt",
+            FileUploader.LOCAL_BEHAVIOUR_COPY,
+            null,
+            true,
+            UploadFileOperation.CREATED_BY_USER,
+            false,
+            false,
+            FileUploader.NameCollisionPolicy.DEFAULT)
+
+        Thread.sleep(LONG_WAIT)
+
+        val result = ReadFileRemoteOperation("/testFile.txt").execute(client)
+        assertTrue(result.isSuccess)
+
+        assertEquals(file.length(), (result.data[0] as RemoteFile).length)
+
+        val ocFile2 = OCFile("/testFile.txt")
+        ocFile2.setStoragePath(getSavePath(account.name) + "/empty.txt")
+
+        FileUploader.uploadUpdateFile(
+            targetContext,
+            account,
+            ocFile2,
+            FileUploader.LOCAL_BEHAVIOUR_COPY,
+            FileUploader.NameCollisionPolicy.RENAME)
+
+        Thread.sleep(SHORT_WAIT)
+
+        val result2 = ReadFileRemoteOperation("/testFile.txt").execute(client)
+        assertTrue(result2.isSuccess)
+
+        assertEquals(file.length(), (result2.data[0] as RemoteFile).length)
+
+        val result3 = ReadFileRemoteOperation("/testFile (2).txt").execute(client)
+        assertTrue(result3.isSuccess)
+
+        assertEquals(ocFile2.fileLength, (result3.data[0] as RemoteFile).length)
+    }
+
+    /**
+     * uploads a file with "keep server" option set, so do nothing
+     */
+    @Test
+    fun testKeepServer() {
+        val file = File(getSavePath(account.name) + "/chunkedFile.txt")
+        val ocUpload = OCUpload(file.absolutePath, "/testFile.txt", account.name)
+
+        assertTrue(UploadFileOperation(
+            uploadsStorageManager,
+            connectivityServiceMock,
+            powerManagementServiceMock,
+            account,
+            null,
+            ocUpload,
+            FileUploader.NameCollisionPolicy.DEFAULT,
+            FileUploader.LOCAL_BEHAVIOUR_COPY,
+            targetContext,
+            false,
+            false)
+            .setRemoteFolderToBeCreated()
+            .execute(client, storageManager).isSuccess)
+
+        val result = ReadFileRemoteOperation("/testFile.txt").execute(client)
+        assertTrue(result.isSuccess)
+
+        assertEquals(file.length(), (result.data[0] as RemoteFile).length)
+
+        val ocUpload2 = OCUpload(getSavePath(account.name) + "/empty.txt", "/testFile.txt", account.name)
+
+        assertFalse(UploadFileOperation(
+            uploadsStorageManager,
+            connectivityServiceMock,
+            powerManagementServiceMock,
+            account,
+            null,
+            ocUpload2,
+            FileUploader.NameCollisionPolicy.CANCEL,
+            FileUploader.LOCAL_BEHAVIOUR_COPY,
+            targetContext,
+            false,
+            false)
+            .execute(client, storageManager).isSuccess)
+
+        val result2 = ReadFileRemoteOperation("/testFile.txt").execute(client)
+        assertTrue(result2.isSuccess)
+
+        assertEquals(file.length(), (result2.data[0] as RemoteFile).length)
+    }
+
+    /**
+     * uploads a file with "keep server" option set, so do nothing
+     */
+    @Test
+    fun testKeepServerStatic() {
+        val file = File(getSavePath(account.name) + "/chunkedFile.txt")
+
+        FileUploader.uploadNewFile(
+            targetContext,
+            account,
+            file.absolutePath,
+            "/testFile.txt",
+            FileUploader.LOCAL_BEHAVIOUR_COPY,
+            null,
+            true,
+            UploadFileOperation.CREATED_BY_USER,
+            false,
+            false,
+            FileUploader.NameCollisionPolicy.DEFAULT)
+
+        Thread.sleep(LONG_WAIT)
+
+        val result = ReadFileRemoteOperation("/testFile.txt").execute(client)
+        assertTrue(result.isSuccess)
+
+        assertEquals(file.length(), (result.data[0] as RemoteFile).length)
+
+        val ocFile2 = OCFile("/testFile.txt")
+        ocFile2.setStoragePath(getSavePath(account.name) + "/empty.txt")
+
+        FileUploader.uploadUpdateFile(
+            targetContext,
+            account,
+            ocFile2,
+            FileUploader.LOCAL_BEHAVIOUR_COPY,
+            FileUploader.NameCollisionPolicy.CANCEL)
+
+        Thread.sleep(SHORT_WAIT)
+
+        val result2 = ReadFileRemoteOperation("/testFile.txt").execute(client)
+        assertTrue(result2.isSuccess)
+
+        assertEquals(file.length(), (result2.data[0] as RemoteFile).length)
+    }
+}

+ 290 - 0
src/androidTest/java/com/owncloud/android/ui/activity/ConflictsResolveActivityIT.java

@@ -0,0 +1,290 @@
+/*
+ *
+ * Nextcloud Android client application
+ *
+ * @author Tobias Kaminsky
+ * Copyright (C) 2020 Tobias Kaminsky
+ * Copyright (C) 2020 Nextcloud GmbH
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+package com.owncloud.android.ui.activity;
+
+import android.content.Intent;
+
+import com.facebook.testing.screenshot.Screenshot;
+import com.nextcloud.client.account.UserAccountManagerImpl;
+import com.owncloud.android.AbstractIT;
+import com.owncloud.android.R;
+import com.owncloud.android.datamodel.FileDataStorageManager;
+import com.owncloud.android.datamodel.OCFile;
+import com.owncloud.android.db.OCUpload;
+import com.owncloud.android.lib.resources.files.UploadFileRemoteOperation;
+import com.owncloud.android.operations.RefreshFolderOperation;
+import com.owncloud.android.ui.dialog.ConflictsResolveDialog;
+import com.owncloud.android.utils.FileStorageUtils;
+
+import org.junit.Rule;
+import org.junit.Test;
+
+import java.io.File;
+import java.io.IOException;
+
+import androidx.fragment.app.DialogFragment;
+import androidx.test.espresso.intent.rule.IntentsTestRule;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+import static junit.framework.TestCase.assertTrue;
+import static org.junit.Assert.assertEquals;
+
+public class ConflictsResolveActivityIT extends AbstractIT {
+    @Rule public IntentsTestRule<ConflictsResolveActivity> activityRule =
+        new IntentsTestRule<>(ConflictsResolveActivity.class, true, false);
+    private boolean returnCode;
+
+    @Test
+    public void screenshotTextFiles() throws InterruptedException {
+        OCUpload newUpload = new OCUpload(FileStorageUtils.getSavePath(account.name) + "/nonEmpty.txt",
+                                          "/newFile.txt",
+                                          account.name);
+
+        OCFile existingFile = new OCFile("/newFile.txt");
+        existingFile.setFileLength(1024000);
+        existingFile.setModificationTimestamp(1582019340);
+
+        FileDataStorageManager storageManager = new FileDataStorageManager(account, targetContext.getContentResolver());
+        storageManager.saveNewFile(existingFile);
+
+        Intent intent = new Intent(targetContext, ConflictsResolveActivity.class);
+        intent.putExtra(ConflictsResolveActivity.EXTRA_FILE, existingFile);
+        intent.putExtra(ConflictsResolveActivity.EXTRA_CONFLICT_UPLOAD, newUpload);
+
+        ConflictsResolveActivity sut = activityRule.launchActivity(intent);
+
+        ConflictsResolveDialog dialog = ConflictsResolveDialog.newInstance(existingFile,
+                                                                           newUpload,
+                                                                           UserAccountManagerImpl
+                                                                               .fromContext(targetContext)
+                                                                               .getUser()
+                                                                          );
+        dialog.showDialog(sut);
+
+        getInstrumentation().waitForIdleSync();
+
+        Thread.sleep(2000);
+
+        Screenshot.snap(dialog.getDialog().getWindow().getDecorView()).record();
+    }
+
+    @Test
+    public void screenshotImages() throws InterruptedException, IOException {
+        FileDataStorageManager storageManager = new FileDataStorageManager(account,
+                                                                           targetContext.getContentResolver());
+
+        OCUpload newUpload = new OCUpload(FileStorageUtils.getSavePath(account.name) + "/nonEmpty.txt",
+                                          "/newFile.txt", account.name);
+
+        File image = getFile("image.jpg");
+
+        assertTrue(new UploadFileRemoteOperation(image.getAbsolutePath(),
+                                                 "/image.jpg",
+                                                 "image/jpg",
+                                                 "10000000").execute(client).isSuccess());
+
+        assertTrue(new RefreshFolderOperation(storageManager.getFileByPath("/"),
+                                              System.currentTimeMillis(),
+                                              false,
+                                              true,
+                                              storageManager,
+                                              account,
+                                              targetContext
+        ).execute(client).isSuccess());
+
+        OCFile existingFile = storageManager.getFileByPath("/image.jpg");
+
+        Intent intent = new Intent(targetContext, ConflictsResolveActivity.class);
+        intent.putExtra(ConflictsResolveActivity.EXTRA_FILE, existingFile);
+        intent.putExtra(ConflictsResolveActivity.EXTRA_CONFLICT_UPLOAD, newUpload);
+
+        ConflictsResolveActivity sut = activityRule.launchActivity(intent);
+
+        ConflictsResolveDialog.OnConflictDecisionMadeListener listener = decision -> {
+
+        };
+
+        ConflictsResolveDialog dialog = ConflictsResolveDialog.newInstance(existingFile,
+                                                                           newUpload,
+                                                                           UserAccountManagerImpl
+                                                                               .fromContext(targetContext)
+                                                                               .getUser()
+                                                                          );
+        dialog.showDialog(sut);
+        dialog.listener = listener;
+
+        getInstrumentation().waitForIdleSync();
+        Thread.sleep(2000);
+
+        Screenshot.snap(dialog.getDialog().getWindow().getDecorView()).record();
+    }
+
+    @Test
+    public void cancel() throws InterruptedException {
+        returnCode = false;
+
+        OCUpload newUpload = new OCUpload(FileStorageUtils.getSavePath(account.name) + "/nonEmpty.txt",
+                                          "/newFile.txt",
+                                          account.name);
+        OCFile existingFile = new OCFile("/newFile.txt");
+        existingFile.setFileLength(1024000);
+        existingFile.setModificationTimestamp(1582019340);
+
+        FileDataStorageManager storageManager = new FileDataStorageManager(account, targetContext.getContentResolver());
+        storageManager.saveNewFile(existingFile);
+
+        Intent intent = new Intent(targetContext, ConflictsResolveActivity.class);
+        intent.putExtra(ConflictsResolveActivity.EXTRA_FILE, existingFile);
+        intent.putExtra(ConflictsResolveActivity.EXTRA_CONFLICT_UPLOAD, newUpload);
+
+        ConflictsResolveActivity sut = activityRule.launchActivity(intent);
+
+        sut.listener = decision -> {
+            assertEquals(decision, ConflictsResolveDialog.Decision.CANCEL);
+            returnCode = true;
+        };
+
+        getInstrumentation().waitForIdleSync();
+        Thread.sleep(2000);
+
+        onView(withText("Cancel")).perform(click());
+
+        assertTrue(returnCode);
+    }
+
+    @Test
+    public void keepExisting() {
+        returnCode = false;
+
+        OCUpload newUpload = new OCUpload(FileStorageUtils.getSavePath(account.name) + "/nonEmpty.txt",
+                                          "/newFile.txt",
+                                          account.name);
+        OCFile existingFile = new OCFile("/newFile.txt");
+        existingFile.setFileLength(1024000);
+        existingFile.setModificationTimestamp(1582019340);
+
+        FileDataStorageManager storageManager = new FileDataStorageManager(account, targetContext.getContentResolver());
+        storageManager.saveNewFile(existingFile);
+
+        Intent intent = new Intent(targetContext, ConflictsResolveActivity.class);
+        intent.putExtra(ConflictsResolveActivity.EXTRA_FILE, existingFile);
+        intent.putExtra(ConflictsResolveActivity.EXTRA_CONFLICT_UPLOAD, newUpload);
+
+        ConflictsResolveActivity sut = activityRule.launchActivity(intent);
+
+        sut.listener = decision -> {
+            assertEquals(decision, ConflictsResolveDialog.Decision.KEEP_SERVER);
+            returnCode = true;
+        };
+
+        getInstrumentation().waitForIdleSync();
+
+        onView(withId(R.id.existing_checkbox)).perform(click());
+
+        DialogFragment dialog = (DialogFragment) sut.getSupportFragmentManager().findFragmentByTag("conflictDialog");
+        Screenshot.snap(dialog.getDialog().getWindow().getDecorView()).record();
+
+        onView(withText("OK")).perform(click());
+
+        assertTrue(returnCode);
+    }
+
+    @Test
+    public void keepNew() {
+        returnCode = false;
+
+        OCUpload newUpload = new OCUpload(FileStorageUtils.getSavePath(account.name) + "/nonEmpty.txt",
+                                          "/newFile.txt",
+                                          account.name);
+        OCFile existingFile = new OCFile("/newFile.txt");
+        existingFile.setFileLength(1024000);
+        existingFile.setModificationTimestamp(1582019340);
+
+        FileDataStorageManager storageManager = new FileDataStorageManager(account, targetContext.getContentResolver());
+        storageManager.saveNewFile(existingFile);
+
+        Intent intent = new Intent(targetContext, ConflictsResolveActivity.class);
+        intent.putExtra(ConflictsResolveActivity.EXTRA_FILE, existingFile);
+        intent.putExtra(ConflictsResolveActivity.EXTRA_CONFLICT_UPLOAD, newUpload);
+
+        ConflictsResolveActivity sut = activityRule.launchActivity(intent);
+
+        sut.listener = decision -> {
+            assertEquals(decision, ConflictsResolveDialog.Decision.KEEP_SERVER);
+            returnCode = true;
+        };
+
+        getInstrumentation().waitForIdleSync();
+
+        onView(withId(R.id.new_checkbox)).perform(click());
+
+        DialogFragment dialog = (DialogFragment) sut.getSupportFragmentManager().findFragmentByTag("conflictDialog");
+        Screenshot.snap(dialog.getDialog().getWindow().getDecorView()).record();
+
+        onView(withText("OK")).perform(click());
+
+        assertTrue(returnCode);
+    }
+
+    @Test
+    public void keepBoth() {
+        returnCode = false;
+
+        OCUpload newUpload = new OCUpload(FileStorageUtils.getSavePath(account.name) + "/nonEmpty.txt",
+                                          "/newFile.txt",
+                                          account.name);
+        OCFile existingFile = new OCFile("/newFile.txt");
+        existingFile.setFileLength(1024000);
+        existingFile.setModificationTimestamp(1582019340);
+
+        FileDataStorageManager storageManager = new FileDataStorageManager(account, targetContext.getContentResolver());
+        storageManager.saveNewFile(existingFile);
+
+        Intent intent = new Intent(targetContext, ConflictsResolveActivity.class);
+        intent.putExtra(ConflictsResolveActivity.EXTRA_FILE, existingFile);
+        intent.putExtra(ConflictsResolveActivity.EXTRA_CONFLICT_UPLOAD, newUpload);
+
+        ConflictsResolveActivity sut = activityRule.launchActivity(intent);
+
+        sut.listener = decision -> {
+            assertEquals(decision, ConflictsResolveDialog.Decision.KEEP_SERVER);
+            returnCode = true;
+        };
+
+        getInstrumentation().waitForIdleSync();
+
+        onView(withId(R.id.existing_checkbox)).perform(click());
+        onView(withId(R.id.new_checkbox)).perform(click());
+
+        DialogFragment dialog = (DialogFragment) sut.getSupportFragmentManager().findFragmentByTag("conflictDialog");
+        Screenshot.snap(dialog.getDialog().getWindow().getDecorView()).record();
+
+        onView(withText("OK")).perform(click());
+
+        assertTrue(returnCode);
+    }
+}

+ 2 - 0
src/main/java/com/owncloud/android/datamodel/OCFile.java

@@ -160,6 +160,7 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
         ownerDisplayName = source.readString();
         mountType = (WebdavEntry.MountType) source.readSerializable();
         richWorkspace = source.readString();
+        previewAvailable = source.readInt() == 1;
     }
 
     @Override
@@ -193,6 +194,7 @@ public class OCFile implements Parcelable, Comparable<OCFile>, ServerFileInterfa
         dest.writeString(ownerDisplayName);
         dest.writeSerializable(mountType);
         dest.writeString(richWorkspace);
+        dest.writeInt(previewAvailable ? 1 : 0);
     }
 
     public String getDecryptedRemotePath() {

+ 9 - 3
src/main/java/com/owncloud/android/operations/UploadFileOperation.java

@@ -285,8 +285,10 @@ public class UploadFileOperation extends SyncOperation {
         return mLocalBehaviour;
     }
 
-    public void setRemoteFolderToBeCreated() {
+    public UploadFileOperation setRemoteFolderToBeCreated() {
         mRemoteFolderToBeCreated = true;
+
+        return this;
     }
 
     public boolean wasRenamed() {
@@ -348,8 +350,10 @@ public class UploadFileOperation extends SyncOperation {
         }
     }
 
-    public void addRenameUploadListener(OnRenameListener listener) {
+    public UploadFileOperation addRenameUploadListener(OnRenameListener listener) {
         mRenameUploadListener = listener;
+
+        return this;
     }
 
     public Context getContext() {
@@ -946,7 +950,9 @@ public class UploadFileOperation extends SyncOperation {
                     mWasRenamed = true;
                     createNewOCFile(mRemotePath);
                     Log_OC.d(TAG, "File renamed as " + mRemotePath);
-                    mRenameUploadListener.onRenameUpload();
+                    if (mRenameUploadListener != null) {
+                        mRenameUploadListener.onRenameUpload();
+                    }
                     break;
                 case OVERWRITE:
                     Log_OC.d(TAG, "Overwriting file");

+ 98 - 61
src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.java

@@ -23,7 +23,10 @@ package com.owncloud.android.ui.activity;
 
 import android.content.Intent;
 import android.os.Bundle;
+import android.widget.Toast;
 
+import com.nextcloud.client.account.User;
+import com.nextcloud.java.util.Optional;
 import com.owncloud.android.datamodel.OCFile;
 import com.owncloud.android.datamodel.UploadsStorageManager;
 import com.owncloud.android.db.OCUpload;
@@ -36,6 +39,9 @@ import com.owncloud.android.ui.dialog.ConflictsResolveDialog.OnConflictDecisionM
 
 import javax.inject.Inject;
 
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentTransaction;
+
 
 /**
  * Wrapper activity which will be launched if keep-in-sync file will be modified by external
@@ -57,6 +63,7 @@ public class ConflictsResolveActivity extends FileActivity implements OnConflict
 
     private OCUpload conflictUpload;
     private int localBehaviour = FileUploader.LOCAL_BEHAVIOUR_FORGET;
+    protected OnConflictDecisionMadeListener listener;
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -73,79 +80,109 @@ public class ConflictsResolveActivity extends FileActivity implements OnConflict
         if (conflictUpload != null) {
             localBehaviour = conflictUpload.getLocalAction();
         }
+
+        listener = new OnConflictDecisionMadeListener() {
+            @Override
+            public void conflictDecisionMade(Decision decision) {
+                OCFile file = getFile();
+
+                switch (decision) {
+                    case CANCEL:
+                        // nothing to do
+                        break;
+                    case KEEP_LOCAL: // Upload
+                        FileUploader.uploadUpdateFile(
+                            getBaseContext(),
+                            getAccount(),
+                            file,
+                            localBehaviour,
+                            FileUploader.NameCollisionPolicy.OVERWRITE
+                                                     );
+
+                        if (conflictUpload != null) {
+                            uploadsStorageManager.removeUpload(conflictUpload);
+                        }
+                        break;
+                    case KEEP_BOTH: // Upload
+                        FileUploader.uploadUpdateFile(
+                            getBaseContext(),
+                            getAccount(),
+                            file,
+                            localBehaviour,
+                            FileUploader.NameCollisionPolicy.RENAME
+                                                     );
+
+                        if (conflictUpload != null) {
+                            uploadsStorageManager.removeUpload(conflictUpload);
+                        }
+                        break;
+                    case KEEP_SERVER: // Download
+                        if (!shouldDeleteLocal()) {
+                            // Overwrite local file
+                            Intent intent = new Intent(getBaseContext(), 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;
+                }
+
+                finish();
+            }
+        };
+    }
+
+    @Override
+    protected void onSaveInstanceState(Bundle outState) {
+        super.onSaveInstanceState(outState);
+
+        outState.putParcelable(EXTRA_CONFLICT_UPLOAD, conflictUpload);
+        outState.putInt(EXTRA_LOCAL_BEHAVIOUR, localBehaviour);
     }
 
     @Override
     public void conflictDecisionMade(Decision decision) {
-        if (decision == Decision.CANCEL) {
-            return;
+        listener.conflictDecisionMade(decision);
+    }
+
+    @Override
+    protected void onStart() {
+        super.onStart();
+        if (getAccount() == null) {
+            finish();
         }
 
         OCFile file = getFile();
+        if (getFile() == null) {
+            Log_OC.e(TAG, "No file received");
+            finish();
+        }
 
-        switch (decision) {
-            case KEEP_LOCAL: // Upload
-                FileUploader.uploadUpdateFile(
-                    this,
-                    getAccount(),
-                    file,
-                    localBehaviour,
-                    FileUploader.NameCollisionPolicy.OVERWRITE
-                );
-
-                if (conflictUpload != null) {
-                    uploadsStorageManager.removeUpload(conflictUpload);
-                }
-                break;
-            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;
+        Optional<User> userOptional = getUser();
+
+        if (!userOptional.isPresent()) {
+            Toast.makeText(this, "Error creating conflict dialog!", Toast.LENGTH_LONG).show();
+            finish();
         }
 
-        finish();
-    }
+        // Check whether the file is contained in the current Account
+        Fragment prev = getSupportFragmentManager().findFragmentByTag("conflictDialog");
 
-    @Override
-    protected void onStart() {
-        super.onStart();
-        if (getAccount() != null) {
-            OCFile file = getFile();
-            if (getFile() == null) {
-                Log_OC.e(TAG, "No file received");
-                finish();
-            } else {
-                // 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
-                    finish();
-                }
-            }
+        FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
+        if (prev != null) {
+            fragmentTransaction.remove(prev);
+        }
+
+        if (getStorageManager().fileExists(file.getRemotePath())) {
+            ConflictsResolveDialog dialog = ConflictsResolveDialog.newInstance(getFile(),
+                                                                               conflictUpload,
+                                                                               userOptional.get());
+            dialog.show(fragmentTransaction, "conflictDialog");
         } else {
+            // Account was changed to a different one - just finish
             finish();
         }
     }

+ 9 - 8
src/main/java/com/owncloud/android/ui/adapter/LocalFileListAdapter.java

@@ -160,7 +160,7 @@ public class LocalFileListAdapter extends RecyclerView.Adapter<RecyclerView.View
                 }
 
                 gridViewHolder.thumbnail.setTag(file.hashCode());
-                setThumbnail(file, gridViewHolder.thumbnail);
+                setThumbnail(file, gridViewHolder.thumbnail, mContext);
 
                 if (file.isDirectory()) {
                     gridViewHolder.checkbox.setVisibility(View.GONE);
@@ -203,16 +203,17 @@ public class LocalFileListAdapter extends RecyclerView.Adapter<RecyclerView.View
         }
     }
 
-    private void setThumbnail(File file, ImageView thumbnailView) {
+    public static void setThumbnail(File file, ImageView thumbnailView, Context context) {
         if (file.isDirectory()) {
-            thumbnailView.setImageDrawable(MimeTypeUtil.getDefaultFolderIcon(mContext));
+            thumbnailView.setImageDrawable(MimeTypeUtil.getDefaultFolderIcon(context));
         } else {
             thumbnailView.setImageResource(R.drawable.file);
 
             /* Cancellation needs do be checked and done before changing the drawable in fileIcon, or
              * {@link ThumbnailsCacheManager#cancelPotentialThumbnailWork} will NEVER cancel any task.
              */
-            boolean allowedToCreateNewThumbnail = ThumbnailsCacheManager.cancelPotentialThumbnailWork(file, thumbnailView);
+            boolean allowedToCreateNewThumbnail = ThumbnailsCacheManager.cancelPotentialThumbnailWork(file,
+                                                                                                      thumbnailView);
 
 
             // get Thumbnail if file is image
@@ -236,9 +237,9 @@ public class LocalFileListAdapter extends RecyclerView.Adapter<RecyclerView.View
                         }
                         final ThumbnailsCacheManager.AsyncThumbnailDrawable asyncDrawable =
                                 new ThumbnailsCacheManager.AsyncThumbnailDrawable(
-                                        mContext.getResources(),
-                                        thumbnail,
-                                        task
+                                    context.getResources(),
+                                    thumbnail,
+                                    task
                                 );
                         thumbnailView.setImageDrawable(asyncDrawable);
                         task.execute(new ThumbnailsCacheManager.ThumbnailGenerationTaskObject(file, null));
@@ -247,7 +248,7 @@ public class LocalFileListAdapter extends RecyclerView.Adapter<RecyclerView.View
                     } // else, already being generated, don't restart it
                 }
             } else {
-                thumbnailView.setImageDrawable(MimeTypeUtil.getFileTypeIcon(null, file.getName(), mContext));
+                thumbnailView.setImageDrawable(MimeTypeUtil.getFileTypeIcon(null, file.getName(), context));
             }
         }
     }

+ 21 - 8
src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java

@@ -27,6 +27,7 @@ package com.owncloud.android.ui.adapter;
 import android.accounts.AccountManager;
 import android.app.Activity;
 import android.content.ContentValues;
+import android.content.Context;
 import android.content.res.Resources;
 import android.graphics.Bitmap;
 import android.graphics.Color;
@@ -353,7 +354,13 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
             boolean gridImage = MimeTypeUtil.isImage(file) || MimeTypeUtil.isVideo(file);
 
             gridViewHolder.thumbnail.setTag(file.getFileId());
-            setThumbnail(file, gridViewHolder.thumbnail);
+            setThumbnail(file,
+                         gridViewHolder.thumbnail,
+                         user,
+                         mStorageManager,
+                         asyncTasks,
+                         gridView,
+                         activity);
 
             if (highlightedItem != null && file.getFileId() == highlightedItem.getFileId()) {
                 gridViewHolder.itemLayout.setBackgroundColor(activity.getResources()
@@ -585,12 +592,18 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
             });
         }
 
-    private void setThumbnail(OCFile file, ImageView thumbnailView) {
+    public static void setThumbnail(OCFile file,
+                                    ImageView thumbnailView,
+                                    User user,
+                                    FileDataStorageManager storageManager,
+                                    List<ThumbnailsCacheManager.ThumbnailGenerationTask> asyncTasks,
+                                    boolean gridView,
+                                    Context context) {
         if (file.isFolder()) {
             thumbnailView.setImageDrawable(MimeTypeUtil
                                                .getFolderTypeIcon(file.isSharedWithMe() || file.isSharedWithSharee(),
                                                                   file.isSharedViaLink(), file.isEncrypted(),
-                                                                  file.getMountType(), activity));
+                                                                  file.getMountType(), context));
         } else {
             if (file.getRemoteId() != null && file.isPreviewAvailable()) {
                 // Thumbnail in cache?
@@ -615,7 +628,7 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
                         try {
                             final ThumbnailsCacheManager.ThumbnailGenerationTask task =
                                 new ThumbnailsCacheManager.ThumbnailGenerationTask(thumbnailView,
-                                                                                   mStorageManager,
+                                                                                   storageManager,
                                                                                    user.toPlatformAccount(),
                                                                                    asyncTasks,
                                                                                    !gridView);
@@ -625,10 +638,10 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
                                     MimeTypeUtil.getFileTypeIcon(file.getMimeType(),
                                                                  file.getFileName(),
                                                                  user.toPlatformAccount(),
-                                                                 activity));
+                                                                 context));
                             }
                             final ThumbnailsCacheManager.AsyncThumbnailDrawable asyncDrawable =
-                                new ThumbnailsCacheManager.AsyncThumbnailDrawable(activity.getResources(),
+                                new ThumbnailsCacheManager.AsyncThumbnailDrawable(context.getResources(),
                                                                                   thumbnail, task);
                             thumbnailView.setImageDrawable(asyncDrawable);
                             asyncTasks.add(task);
@@ -641,13 +654,13 @@ public class OCFileListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
                 }
 
                 if ("image/png".equalsIgnoreCase(file.getMimeType())) {
-                    thumbnailView.setBackgroundColor(activity.getResources().getColor(R.color.bg_default));
+                    thumbnailView.setBackgroundColor(context.getResources().getColor(R.color.bg_default));
                 }
             } else {
                 thumbnailView.setImageDrawable(MimeTypeUtil.getFileTypeIcon(file.getMimeType(),
                                                                             file.getFileName(),
                                                                             user.toPlatformAccount(),
-                                                                            activity));
+                                                                            context));
             }
         }
     }

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

@@ -22,12 +22,34 @@
 package com.owncloud.android.ui.dialog;
 
 import android.app.Dialog;
+import android.content.Context;
 import android.content.DialogInterface;
 import android.os.Bundle;
-
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.nextcloud.client.account.User;
 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.db.OCUpload;
+import com.owncloud.android.lib.common.utils.Log_OC;
+import com.owncloud.android.ui.adapter.LocalFileListAdapter;
+import com.owncloud.android.ui.adapter.OCFileListAdapter;
+import com.owncloud.android.utils.DisplayUtils;
+import com.owncloud.android.utils.ThemeUtils;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.List;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
 import androidx.appcompat.app.AlertDialog;
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.fragment.app.DialogFragment;
@@ -40,6 +62,16 @@ import androidx.fragment.app.FragmentTransaction;
  */
 public class ConflictsResolveDialog extends DialogFragment {
 
+    private OCFile existingFile;
+    private File newFile;
+    public OnConflictDecisionMadeListener listener;
+    private User user;
+    private List<ThumbnailsCacheManager.ThumbnailGenerationTask> asyncTasks = new ArrayList<>();
+
+    private static final String KEY_NEW_FILE = "file";
+    private static final String KEY_EXISTING_FILE = "ocfile";
+    private static final String KEY_USER = "user";
+
     public enum Decision {
         CANCEL,
         KEEP_BOTH,
@@ -47,42 +79,133 @@ public class ConflictsResolveDialog extends DialogFragment {
         KEEP_SERVER,
     }
 
-    private final OnConflictDecisionMadeListener listener;
-    private final boolean canKeepServer;
+    public static ConflictsResolveDialog newInstance(OCFile existingFile, OCUpload conflictUpload, User user) {
+        ConflictsResolveDialog dialog = new ConflictsResolveDialog();
+
+        Bundle args = new Bundle();
+        args.putParcelable(KEY_EXISTING_FILE, existingFile);
+        args.putSerializable(KEY_NEW_FILE, new File(conflictUpload.getLocalPath()));
+        args.putParcelable(KEY_USER, user);
+        dialog.setArguments(args);
+
+        return dialog;
+    }
+
+    @Override
+    public void onAttach(@NonNull Context context) {
+        super.onAttach(context);
+
+        try {
+            listener = (OnConflictDecisionMadeListener) context;
+        } catch (ClassCastException e) {
+            throw new ClassCastException("Activity of this dialog must implement OnConflictDecisionMadeListener");
+        }
+    }
+
+    @Override
+    public void onStart() {
+        super.onStart();
+
+        AlertDialog alertDialog = (AlertDialog) getDialog();
+
+        if (alertDialog == null) {
+            Toast.makeText(getContext(), "Failed to create conflict dialog", Toast.LENGTH_LONG).show();
+            return;
+        }
+
+        int color = ThemeUtils.primaryAccentColor(getContext());
+        alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setTextColor(color);
+        alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE).setTextColor(color);
+    }
+
+    @Override
+    public void onCreate(@Nullable Bundle savedInstanceState) {
+        super.onCreate(savedInstanceState);
+
+        if (savedInstanceState != null) {
+            existingFile = savedInstanceState.getParcelable(KEY_EXISTING_FILE);
+            newFile = (File) savedInstanceState.getSerializable(KEY_NEW_FILE);
+            user = savedInstanceState.getParcelable(KEY_USER);
+        } else if (getArguments() != null) {
+            existingFile = getArguments().getParcelable(KEY_EXISTING_FILE);
+            newFile = (File) getArguments().getSerializable(KEY_NEW_FILE);
+            user = getArguments().getParcelable(KEY_USER);
+        } else {
+            Toast.makeText(getContext(), "Failed to create conflict dialog", Toast.LENGTH_LONG).show();
+        }
+    }
 
-    public ConflictsResolveDialog(OnConflictDecisionMadeListener listener, boolean canKeepServer) {
-        this.listener = listener;
-        this.canKeepServer = canKeepServer;
+    @Override
+    public void onSaveInstanceState(@NonNull Bundle outState) {
+        super.onSaveInstanceState(outState);
+
+        outState.putParcelable(KEY_EXISTING_FILE, existingFile);
+        outState.putSerializable(KEY_NEW_FILE, newFile);
+        outState.putParcelable(KEY_USER, user);
     }
 
     @NonNull
     @Override
     public Dialog onCreateDialog(Bundle savedInstanceState) {
-        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);
-                                          }
-                                      });
-        }
+        // Inflate the layout for the dialog
+        LayoutInflater inflater = getActivity().getLayoutInflater();
+        View view = inflater.inflate(R.layout.conflict_resolve_dialog, null);
+        int accentColor = ThemeUtils.primaryAccentColor(getContext());
+
+        // Build the dialog
+        AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+        builder.setView(view)
+            .setPositiveButton(R.string.common_ok, (dialog, which) -> {
+                if (listener != null) {
+                    CheckBox newFile = view.findViewById(R.id.new_checkbox);
+                    CheckBox existingFile = view.findViewById(R.id.existing_checkbox);
+
+                    if (newFile.isSelected() && existingFile.isSelected()) {
+                        listener.conflictDecisionMade(Decision.KEEP_BOTH);
+                    } else if (newFile.isSelected()) {
+                        listener.conflictDecisionMade(Decision.KEEP_LOCAL);
+                    } else {
+                        listener.conflictDecisionMade(Decision.KEEP_SERVER);
+                    }
+                }
+            })
+            .setNegativeButton(R.string.common_cancel, (dialog, which) -> {
+                if (listener != null) {
+                    listener.conflictDecisionMade(Decision.CANCEL);
+                }
+            })
+            .setTitle(ThemeUtils.getColoredTitle(getResources().getString(R.string.conflict_message_headline),
+                                                 accentColor));
+
+        // set info for new file
+        TextView newSize = view.findViewById(R.id.new_size);
+        newSize.setText(DisplayUtils.bytesToHumanReadable(newFile.length()));
+
+        TextView newTimestamp = view.findViewById(R.id.new_timestamp);
+        newTimestamp.setText(DisplayUtils.getRelativeTimestamp(getContext(), newFile.lastModified()));
+
+        ImageView newThumbnail = view.findViewById(R.id.new_thumbnail);
+        newThumbnail.setTag(newFile.hashCode());
+        LocalFileListAdapter.setThumbnail(newFile, newThumbnail, getContext());
+
+        // set info for existing file
+        TextView existingSize = view.findViewById(R.id.existing_size);
+        existingSize.setText(DisplayUtils.bytesToHumanReadable(existingFile.getFileLength()));
+
+        TextView existingTimestamp = view.findViewById(R.id.existing_timestamp);
+        existingTimestamp.setText(DisplayUtils.getRelativeTimestamp(getContext(),
+                                                                    existingFile.getModificationTimestamp()));
+
+        ImageView existingThumbnail = view.findViewById(R.id.existing_thumbnail);
+        existingThumbnail.setTag(existingFile.getFileId());
+        OCFileListAdapter.setThumbnail(existingFile,
+                                       view.findViewById(R.id.existing_thumbnail),
+                                       user,
+                                       new FileDataStorageManager(user.toPlatformAccount(),
+                                                                  requireContext().getContentResolver()),
+                                       asyncTasks,
+                                       false,
+                                       getContext());
 
         return builder.create();
     }
@@ -99,7 +222,7 @@ public class ConflictsResolveDialog extends DialogFragment {
     }
 
     @Override
-    public void onCancel(DialogInterface dialog) {
+    public void onCancel(@NonNull DialogInterface dialog) {
         if (listener != null) {
             listener.conflictDecisionMade(Decision.CANCEL);
         }
@@ -108,4 +231,21 @@ public class ConflictsResolveDialog extends DialogFragment {
     public interface OnConflictDecisionMadeListener {
         void conflictDecisionMade(Decision decision);
     }
+
+    @Override
+    public void onStop() {
+        super.onStop();
+
+        for (ThumbnailsCacheManager.ThumbnailGenerationTask task : asyncTasks) {
+            if (task != null) {
+                task.cancel(true);
+                if (task.getGetMethod() != null) {
+                    Log_OC.d(this, "cancel: abort get method directly");
+                    task.getGetMethod().abort();
+                }
+            }
+        }
+
+        asyncTasks.clear();
+    }
 }

+ 110 - 0
src/main/res/layout/conflict_resolve_dialog.xml

@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~
+  ~ Nextcloud Android client application
+  ~
+  ~ @author Tobias Kaminsky
+  ~ Copyright (C) 2020 Tobias Kaminsky
+  ~ Copyright (C) 2020 Nextcloud GmbH
+  ~
+  ~ This program is free software: you can redistribute it and/or modify
+  ~ it under the terms of the GNU Affero General Public License as published by
+  ~ the Free Software Foundation, either version 3 of the License, or
+  ~ (at your option) any later version.
+  ~
+  ~ This program is distributed in the hope that it will be useful,
+  ~ but WITHOUT ANY WARRANTY; without even the implied warranty of
+  ~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+  ~ GNU Affero General Public License for more details.
+  ~
+  ~ You should have received a copy of the GNU Affero General Public License
+  ~ along with this program. If not, see <https://www.gnu.org/licenses/>.
+  -->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:gravity="clip_horizontal"
+    android:orientation="vertical"
+    android:padding="@dimen/standard_padding">
+
+    <TextView
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:text="@string/conflict_message_description" />
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:orientation="horizontal"
+        android:baselineAligned="false">
+
+        <LinearLayout
+            android:id="@+id/newFileContainer"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:orientation="vertical">
+
+            <CheckBox
+                android:id="@+id/new_checkbox"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="@string/conflict_new_file" />
+
+            <ImageView
+                android:id="@+id/new_thumbnail"
+                android:layout_width="match_parent"
+                android:layout_height="80dp"
+                android:layout_margin="@dimen/standard_half_margin"
+                android:src="@drawable/file_image"
+                android:contentDescription="@string/thumbnail_for_new_file_desc" />
+
+            <TextView
+                android:id="@+id/new_timestamp"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                tools:text="12. Dec 2020 - 23:10:20" />
+
+            <TextView
+                android:id="@+id/new_size"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                tools:text="5 Mb" />
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/existingFileContainer"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:orientation="vertical">
+
+            <CheckBox
+                android:id="@+id/existing_checkbox"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:text="@string/conflict_already_existing_file" />
+
+            <ImageView
+                android:id="@+id/existing_thumbnail"
+                android:layout_width="match_parent"
+                android:layout_height="80dp"
+                android:layout_margin="@dimen/standard_half_margin"
+                android:src="@drawable/file_image"
+                android:contentDescription="@string/thumbnail_for_existing_file_description" />
+
+            <TextView
+                android:id="@+id/existing_timestamp"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                tools:text="10. Dec 2020 - 10:10:10" />
+
+            <TextView
+                android:id="@+id/existing_size"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                tools:text="3 Mb" />
+        </LinearLayout>
+    </LinearLayout>
+
+</LinearLayout>

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

@@ -327,11 +327,6 @@
     <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>
-    <string name="conflict_message">Which files do you want to keep? If you select both versions, the local file will have a number appended to its name.</string>
-    <string name="conflict_keep_both">Keep both</string>
-    <string name="conflict_use_local_version">local version</string>
-    <string name="conflict_use_server_version">server version</string>
 
     <string name="preview_sorry">Sorry</string>
     <string name="preview_image_description">Image preview</string>
@@ -924,6 +919,11 @@
     <string name="sync_not_enough_space_dialog_action_free_space">Free up space</string>
     <string name="sync_not_enough_space_dialog_placeholder">%1$s is %2$s, but there is only %3$s available on device.</string>
     <string name="sync_not_enough_space_dialog_title">Not enough space</string>
-
+    <string name="conflict_message_headline">Which files do you want to keep?</string>
+    <string name="conflict_message_description">If you select both versions, the local file will have a number appended to its name.</string>
+    <string name="conflict_new_file">New file</string>
+    <string name="conflict_already_existing_file">Already existing file</string>
+    <string name="thumbnail_for_new_file_desc">Thumbnail for new file</string>
+    <string name="thumbnail_for_existing_file_description">Thumbnail for existing file</string>
     <string name="invalid_url">Invalid URL</string>
 </resources>