Enhance conflict dialog

- fix bug
- add ui tests
- add instrumented tests

Signed-off-by: tobiasKaminsky <tobias@kaminsky.me>
This commit is contained in:
tobiasKaminsky 2020-02-19 08:59:57 +01:00
parent 3ae41ec9fa
commit ae08a42f8a
No known key found for this signature in database
GPG key ID: 0E00D4D47D0C5AF7
19 changed files with 1140 additions and 45 deletions

View file

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View file

@ -0,0 +1,379 @@
/*
*
* 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
class FileUploaderIT : AbstractIT() {
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 ocUpload = OCUpload(getSavePath(account.name) + "/chunkedFile.txt",
"/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(14000000, (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() {
FileUploader.uploadNewFile(
targetContext,
account,
getSavePath(account.name) + "/chunkedFile.txt",
"/testFile.txt",
FileUploader.LOCAL_BEHAVIOUR_COPY,
null,
true,
UploadFileOperation.CREATED_BY_USER,
false,
false,
FileUploader.NameCollisionPolicy.DEFAULT)
Thread.sleep(20000)
val result = ReadFileRemoteOperation("/testFile.txt").execute(client)
assertTrue(result.isSuccess)
assertEquals(14000000, (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(5000)
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 ocUpload = OCUpload(getSavePath(account.name) + "/chunkedFile.txt",
"/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(14000000, (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.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(14000000, (result2.data[0] as RemoteFile).length)
val result3 = ReadFileRemoteOperation("/testFile (2).txt").execute(client)
assertTrue(result3.isSuccess)
assertEquals(0, (result3.data[0] as RemoteFile).length)
assertTrue(renameListenerWasTriggered)
}
/**
* uploads a file, uploads another one with automatically (2) added, check
*/
@Test
fun testKeepBothStatic() {
FileUploader.uploadNewFile(
targetContext,
account,
getSavePath(account.name) + "/chunkedFile.txt",
"/testFile.txt",
FileUploader.LOCAL_BEHAVIOUR_COPY,
null,
true,
UploadFileOperation.CREATED_BY_USER,
false,
false,
FileUploader.NameCollisionPolicy.DEFAULT)
Thread.sleep(20000)
val result = ReadFileRemoteOperation("/testFile.txt").execute(client)
assertTrue(result.isSuccess)
assertEquals(14000000, (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(5000)
val result2 = ReadFileRemoteOperation("/testFile.txt").execute(client)
assertTrue(result2.isSuccess)
assertEquals(14000000, (result2.data[0] as RemoteFile).length)
val result3 = ReadFileRemoteOperation("/testFile (2).txt").execute(client)
assertTrue(result3.isSuccess)
assertEquals(0, (result3.data[0] as RemoteFile).length)
}
/**
* uploads a file with "keep server" option set, so do nothing
*/
@Test
fun testKeepServer() {
val ocUpload = OCUpload(getSavePath(account.name) + "/chunkedFile.txt",
"/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(14000000, (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(14000000, (result2.data[0] as RemoteFile).length)
}
/**
* uploads a file with "keep server" option set, so do nothing
*/
@Test
fun testKeepServerStatic() {
FileUploader.uploadNewFile(
targetContext,
account,
getSavePath(account.name) + "/chunkedFile.txt",
"/testFile.txt",
FileUploader.LOCAL_BEHAVIOUR_COPY,
null,
true,
UploadFileOperation.CREATED_BY_USER,
false,
false,
FileUploader.NameCollisionPolicy.DEFAULT)
Thread.sleep(20000)
val result = ReadFileRemoteOperation("/testFile.txt").execute(client)
assertTrue(result.isSuccess)
assertEquals(14000000, (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(5000)
val result2 = ReadFileRemoteOperation("/testFile.txt").execute(client)
assertTrue(result2.isSuccess)
assertEquals(14000000, (result2.data[0] as RemoteFile).length)
}
}

View file

@ -0,0 +1,328 @@
/*
*
* 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.nextcloud.java.util.Optional;
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.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.OnConflictDecisionMadeListener listener = decision -> {
};
ConflictsResolveDialog dialog = new ConflictsResolveDialog(listener,
existingFile,
newUpload,
Optional.of(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 = new ConflictsResolveDialog(listener,
existingFile,
newUpload,
Optional.of(UserAccountManagerImpl
.fromContext(targetContext)
.getUser()
));
dialog.showDialog(sut);
getInstrumentation().waitForIdleSync();
Thread.sleep(10000);
Screenshot.snap(dialog.getDialog().getWindow().getDecorView()).record();
}
@Test
public void cancel() {
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);
ConflictsResolveDialog.OnConflictDecisionMadeListener listener = decision -> {
assertEquals(decision, ConflictsResolveDialog.Decision.CANCEL);
returnCode = true;
};
ConflictsResolveDialog dialog = new ConflictsResolveDialog(listener,
existingFile,
newUpload,
Optional.of(UserAccountManagerImpl
.fromContext(targetContext)
.getUser()
));
dialog.showDialog(sut);
getInstrumentation().waitForIdleSync();
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);
ConflictsResolveDialog.OnConflictDecisionMadeListener listener = decision -> {
assertEquals(decision, ConflictsResolveDialog.Decision.KEEP_SERVER);
returnCode = true;
};
ConflictsResolveDialog dialog = new ConflictsResolveDialog(listener,
existingFile,
newUpload,
Optional.of(UserAccountManagerImpl
.fromContext(targetContext)
.getUser()
));
dialog.showDialog(sut);
getInstrumentation().waitForIdleSync();
onView(withId(R.id.existing_checkbox)).perform(click());
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);
ConflictsResolveDialog.OnConflictDecisionMadeListener listener = decision -> {
assertEquals(decision, ConflictsResolveDialog.Decision.KEEP_SERVER);
returnCode = true;
};
ConflictsResolveDialog dialog = new ConflictsResolveDialog(listener,
existingFile,
newUpload,
Optional.of(UserAccountManagerImpl
.fromContext(targetContext)
.getUser()
));
dialog.showDialog(sut);
getInstrumentation().waitForIdleSync();
onView(withId(R.id.new_checkbox)).perform(click());
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);
ConflictsResolveDialog.OnConflictDecisionMadeListener listener = decision -> {
assertEquals(decision, ConflictsResolveDialog.Decision.KEEP_SERVER);
returnCode = true;
};
ConflictsResolveDialog dialog = new ConflictsResolveDialog(listener,
existingFile,
newUpload,
Optional.of(UserAccountManagerImpl
.fromContext(targetContext)
.getUser()
));
dialog.showDialog(sut);
getInstrumentation().waitForIdleSync();
onView(withId(R.id.existing_checkbox)).perform(click());
onView(withId(R.id.new_checkbox)).perform(click());
Screenshot.snap(dialog.getDialog().getWindow().getDecorView()).record();
onView(withText("OK")).perform(click());
assertTrue(returnCode);
}
}

View file

@ -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() {

View file

@ -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);
if (mRenameUploadListener != null) {
mRenameUploadListener.onRenameUpload();
}
break;
case OVERWRITE:
Log_OC.d(TAG, "Overwriting file");

View file

@ -36,6 +36,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
@ -58,6 +61,10 @@ public class ConflictsResolveActivity extends FileActivity implements OnConflict
private OCUpload conflictUpload;
private int localBehaviour = FileUploader.LOCAL_BEHAVIOUR_FORGET;
// TODO rotate when conflict dialog open
// TODO cancel leads to white activity?!
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@ -76,14 +83,21 @@ public class ConflictsResolveActivity extends FileActivity implements OnConflict
}
@Override
public void conflictDecisionMade(Decision decision) {
if (decision == Decision.CANCEL) {
return;
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) {
OCFile file = getFile();
switch (decision) {
case CANCEL:
// nothing to do
break;
case KEEP_LOCAL: // Upload
FileUploader.uploadUpdateFile(
this,
@ -137,14 +151,23 @@ public class ConflictsResolveActivity extends FileActivity implements OnConflict
finish();
} else {
// Check whether the file is contained in the current Account
Fragment prev = getSupportFragmentManager().findFragmentByTag("conflictDialog");
FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction();
if (prev == null) {
if (getStorageManager().fileExists(file.getRemotePath())) {
ConflictsResolveDialog dialog = new ConflictsResolveDialog(this, !this.shouldDeleteLocal());
dialog.showDialog(this);
ConflictsResolveDialog dialog = new ConflictsResolveDialog(this,
getFile(),
conflictUpload,
getUser()
);
dialog.show(fragmentTransaction, "conflictDialog");
} else {
// Account was changed to a different one - just finish
finish();
}
}
}
} else {
finish();
}

View file

@ -23,11 +23,36 @@ package com.owncloud.android.ui.dialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.graphics.Bitmap;
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.nextcloud.java.util.Optional;
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.utils.BitmapUtils;
import com.owncloud.android.utils.DisplayUtils;
import com.owncloud.android.utils.MimeTypeUtil;
import com.owncloud.android.utils.ThemeUtils;
import org.jetbrains.annotations.NotNull;
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 +65,12 @@ import androidx.fragment.app.FragmentTransaction;
*/
public class ConflictsResolveDialog extends DialogFragment {
private OCFile existingFile;
private File newFile;
private OnConflictDecisionMadeListener listener;
private User user;
private List<ThumbnailsCacheManager.ThumbnailGenerationTask> asyncTasks = new ArrayList<>();
public enum Decision {
CANCEL,
KEEP_BOTH,
@ -47,42 +78,116 @@ public class ConflictsResolveDialog extends DialogFragment {
KEEP_SERVER,
}
private final OnConflictDecisionMadeListener listener;
private final boolean canKeepServer;
public ConflictsResolveDialog() {
// needed by Android
existingFile = null;
newFile = null;
listener = null;
user = null;
}
public ConflictsResolveDialog(OnConflictDecisionMadeListener listener, boolean canKeepServer) {
public ConflictsResolveDialog(OnConflictDecisionMadeListener listener,
OCFile file,
OCUpload conflictUpload, Optional<User> user) {
this.listener = listener;
this.canKeepServer = canKeepServer;
this.existingFile = file;
this.newFile = new File(conflictUpload.getLocalPath());
this.user = user.get();
}
@Override
public void onStart() {
super.onStart();
int color = ThemeUtils.primaryAccentColor(getContext());
AlertDialog alertDialog = (AlertDialog) getDialog();
if (alertDialog == null || user == null) {
Toast.makeText(getContext(), "Failed to create conflict dialog", Toast.LENGTH_LONG).show();
return;
}
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) {
newFile = (File) savedInstanceState.getSerializable("file");
existingFile = savedInstanceState.getParcelable("ocfile");
user = savedInstanceState.getParcelable("user");
listener = (OnConflictDecisionMadeListener) getActivity();
}
}
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable("file", newFile);
outState.putParcelable("ocfile", existingFile);
outState.putParcelable("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);
}
});
// 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());
if (this.canKeepServer) {
builder.setNegativeButton(R.string.conflict_use_server_version,
(dialog, which) -> {
// 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());
setThumbnail(newFile, newThumbnail);
// 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());
setThumbnail(existingFile, view.findViewById(R.id.existing_thumbnail));
return builder.create();
}
@ -99,7 +204,7 @@ public class ConflictsResolveDialog extends DialogFragment {
}
@Override
public void onCancel(DialogInterface dialog) {
public void onCancel(@NotNull DialogInterface dialog) {
if (listener != null) {
listener.conflictDecisionMade(Decision.CANCEL);
}
@ -108,4 +213,137 @@ public class ConflictsResolveDialog extends DialogFragment {
public interface OnConflictDecisionMadeListener {
void conflictDecisionMade(Decision decision);
}
private void setThumbnail(OCFile file, ImageView thumbnailView) {
if (file.isFolder()) {
thumbnailView.setImageDrawable(MimeTypeUtil
.getFolderTypeIcon(file.isSharedWithMe() || file.isSharedWithSharee(),
file.isSharedViaLink(),
file.isEncrypted(),
file.getMountType(),
getContext()));
} else {
if (file.getRemoteId() != null && file.isPreviewAvailable()) {
// Thumbnail in cache?
Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(
ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.getRemoteId()
);
if (thumbnail != null && !file.isUpdateThumbnailNeeded()) {
if (MimeTypeUtil.isVideo(file)) {
Bitmap withOverlay = ThumbnailsCacheManager.addVideoOverlay(thumbnail);
thumbnailView.setImageBitmap(withOverlay);
} else {
BitmapUtils.setRoundedBitmap(thumbnail, thumbnailView);
}
} else {
// generate new thumbnail
if (ThumbnailsCacheManager.cancelPotentialThumbnailWork(file, thumbnailView)) {
try {
FileDataStorageManager storageManager =
new FileDataStorageManager(user.toPlatformAccount(),
requireContext().getContentResolver());
final ThumbnailsCacheManager.ThumbnailGenerationTask task =
new ThumbnailsCacheManager.ThumbnailGenerationTask(thumbnailView,
storageManager,
user.toPlatformAccount(),
asyncTasks,
true);
if (thumbnail == null) {
thumbnail = BitmapUtils.drawableToBitmap(
MimeTypeUtil.getFileTypeIcon(file.getMimeType(),
file.getFileName(),
user.toPlatformAccount(),
getContext()));
}
final ThumbnailsCacheManager.AsyncThumbnailDrawable asyncDrawable =
new ThumbnailsCacheManager.AsyncThumbnailDrawable(getResources(),
thumbnail, task);
thumbnailView.setImageDrawable(asyncDrawable);
task.execute(new ThumbnailsCacheManager.ThumbnailGenerationTaskObject(file,
file.getRemoteId()));
} catch (IllegalArgumentException e) {
Log_OC.d(this, "ThumbnailGenerationTask : " + e.getMessage());
}
}
}
if ("image/png".equalsIgnoreCase(file.getMimeType())) {
thumbnailView.setBackgroundColor(getResources().getColor(R.color.bg_default));
}
} else {
thumbnailView.setImageDrawable(MimeTypeUtil.getFileTypeIcon(file.getMimeType(),
file.getFileName(),
user.toPlatformAccount(),
getContext()));
}
}
}
private void setThumbnail(File file, ImageView thumbnailView) {
if (file.isDirectory()) {
thumbnailView.setImageDrawable(MimeTypeUtil.getDefaultFolderIcon(getContext()));
} 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);
// get Thumbnail if file is image
if (MimeTypeUtil.isImage(file)) {
// Thumbnail in Cache?
Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache(
ThumbnailsCacheManager.PREFIX_THUMBNAIL + file.hashCode()
);
if (thumbnail != null) {
thumbnailView.setImageBitmap(thumbnail);
} else {
// generate new Thumbnail
if (allowedToCreateNewThumbnail) {
final ThumbnailsCacheManager.ThumbnailGenerationTask task =
new ThumbnailsCacheManager.ThumbnailGenerationTask(thumbnailView);
if (MimeTypeUtil.isVideo(file)) {
thumbnail = ThumbnailsCacheManager.mDefaultVideo;
} else {
thumbnail = ThumbnailsCacheManager.mDefaultImg;
}
final ThumbnailsCacheManager.AsyncThumbnailDrawable asyncDrawable =
new ThumbnailsCacheManager.AsyncThumbnailDrawable(
getResources(),
thumbnail,
task
);
thumbnailView.setImageDrawable(asyncDrawable);
task.execute(new ThumbnailsCacheManager.ThumbnailGenerationTaskObject(file, null));
Log_OC.v(this, "Executing task to generate a new thumbnail");
} // else, already being generated, don't restart it
}
} else {
thumbnailView.setImageDrawable(MimeTypeUtil.getFileTypeIcon(null, file.getName(), getContext()));
}
}
}
@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();
}
}

View file

@ -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>

View file

@ -328,7 +328,7 @@
<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_message"></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>
@ -924,6 +924,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>