Merge pull request #4788 from nextcloud/instantupload_all

Setting to also upload existing files
This commit is contained in:
Tobias Kaminsky 2020-02-18 08:45:03 +01:00 committed by GitHub
commit 5d428fe9a2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
34 changed files with 1576 additions and 1263 deletions

View file

@ -1 +1 @@
385
383

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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