diff --git a/scripts/analysis/findbugs-results.txt b/scripts/analysis/findbugs-results.txt index df90c3c76ca4..f13865781915 100644 --- a/scripts/analysis/findbugs-results.txt +++ b/scripts/analysis/findbugs-results.txt @@ -1 +1 @@ -385 +383 diff --git a/src/androidTest/java/com/owncloud/android/UploadIT.java b/src/androidTest/java/com/owncloud/android/UploadIT.java index 1d468e7162aa..90df24d6daa2 100644 --- a/src/androidTest/java/com/owncloud/android/UploadIT.java +++ b/src/androidTest/java/com/owncloud/android/UploadIT.java @@ -120,7 +120,7 @@ public RemoteOperationResult testUpload(OCUpload ocUpload) { account, null, ocUpload, - false, + FileUploader.NameCollisionPolicy.DEFAULT, FileUploader.LOCAL_BEHAVIOUR_COPY, targetContext, false, @@ -140,17 +140,17 @@ public void testUploadInNonExistingFolder() { 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 diff --git a/src/main/java/com/owncloud/android/MainApp.java b/src/main/java/com/owncloud/android/MainApp.java index fe020d438ca9..37ec76bc0258 100644 --- a/src/main/java/com/owncloud/android/MainApp.java +++ b/src/main/java/com/owncloud/android/MainApp.java @@ -773,7 +773,7 @@ private static void initiateExistingAutoUploadEntries(Clock clock) { for (SyncedFolder syncedFolder : syncedFolderProvider.getSyncedFolders()) { if (syncedFolder.isEnabled()) { - FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder); + FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolder, true); } } diff --git a/src/main/java/com/owncloud/android/datamodel/SyncedFolder.java b/src/main/java/com/owncloud/android/datamodel/SyncedFolder.java index 20a72eed10b0..33a42b867dd7 100644 --- a/src/main/java/com/owncloud/android/datamodel/SyncedFolder.java +++ b/src/main/java/com/owncloud/android/datamodel/SyncedFolder.java @@ -39,6 +39,7 @@ public class SyncedFolder implements Serializable, Cloneable { @Getter @Setter private String remotePath; @Getter @Setter private boolean wifiOnly; @Getter @Setter private boolean chargingOnly; + @Getter @Setter private boolean existing; @Getter @Setter private boolean subfolderByDate; @Getter @Setter private String account; @Getter @Setter private int uploadAction; @@ -54,6 +55,7 @@ public class SyncedFolder implements Serializable, Cloneable { * @param remotePath remote path * @param wifiOnly upload on wifi only flag * @param chargingOnly upload on charging only + * @param existing upload existing files * @param subfolderByDate create sub-folders by date (month) * @param account the account owning the synced folder * @param uploadAction the action to be done after the upload @@ -66,6 +68,7 @@ public SyncedFolder(String localPath, String remotePath, boolean wifiOnly, boolean chargingOnly, + boolean existing, boolean subfolderByDate, String account, int uploadAction, @@ -73,8 +76,8 @@ public SyncedFolder(String localPath, 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 @@ protected SyncedFolder(long id, String remotePath, boolean wifiOnly, boolean chargingOnly, + boolean existing, boolean subfolderByDate, String account, int uploadAction, @@ -99,6 +103,7 @@ protected SyncedFolder(long id, this.remotePath = remotePath; this.wifiOnly = wifiOnly; this.chargingOnly = chargingOnly; + this.existing = existing; this.subfolderByDate = subfolderByDate; this.account = account; this.uploadAction = uploadAction; diff --git a/src/main/java/com/owncloud/android/datamodel/SyncedFolderDisplayItem.java b/src/main/java/com/owncloud/android/datamodel/SyncedFolderDisplayItem.java index 09521e3bef4a..b886954e1c08 100644 --- a/src/main/java/com/owncloud/android/datamodel/SyncedFolderDisplayItem.java +++ b/src/main/java/com/owncloud/android/datamodel/SyncedFolderDisplayItem.java @@ -45,6 +45,7 @@ public class SyncedFolderDisplayItem extends SyncedFolder { * @param remotePath remote path * @param wifiOnly upload on wifi only flag * @param chargingOnly upload on charging only + * @param existing also upload existing * @param subfolderByDate create sub-folders by date (month) * @param account the account owning the synced folder * @param uploadAction the action to be done after the upload @@ -60,6 +61,7 @@ public SyncedFolderDisplayItem(long id, String remotePath, boolean wifiOnly, boolean chargingOnly, + boolean existing, boolean subfolderByDate, String account, int uploadAction, @@ -70,8 +72,8 @@ public SyncedFolderDisplayItem(long id, 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 SyncedFolderDisplayItem(long id, 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; } } diff --git a/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java b/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java index f5fc3009d777..118c60e44a31 100644 --- a/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java +++ b/src/main/java/com/owncloud/android/datamodel/SyncedFolderProvider.java @@ -342,6 +342,8 @@ private SyncedFolder createSyncedFolderFromCursor(Cursor cursor) { 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 @@ private SyncedFolder createSyncedFolderFromCursor(Cursor cursor) { 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 @@ private ContentValues createContentValuesFromSyncedFolder(SyncedFolder syncedFol 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()); diff --git a/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java b/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java index c3e47e95fa74..e66a99b81df4 100644 --- a/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java +++ b/src/main/java/com/owncloud/android/datamodel/UploadsStorageManager.java @@ -84,7 +84,7 @@ public long storeUpload(OCUpload ocUpload) { 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 @@ private OCUpload createOCUploadFromCursor(Cursor c) { 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))); diff --git a/src/main/java/com/owncloud/android/db/OCUpload.java b/src/main/java/com/owncloud/android/db/OCUpload.java index c5259307c9f9..3d9b5b628903 100644 --- a/src/main/java/com/owncloud/android/db/OCUpload.java +++ b/src/main/java/com/owncloud/android/db/OCUpload.java @@ -77,9 +77,9 @@ public class OCUpload implements Parcelable { @Getter @Setter private int localAction; /** - * Overwrite destination file? + * What to do in case of name collision. */ - @Getter @Setter private boolean forceOverwrite; + @Getter @Setter private FileUploader.NameCollisionPolicy nameCollisionPolicy; /** * Create destination folder? @@ -172,7 +172,7 @@ private void resetData() { 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 @@ private void readFromParcel(Parcel source) { 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 void writeToParcel(Parcel dest, int flags) { 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); diff --git a/src/main/java/com/owncloud/android/db/ProviderMeta.java b/src/main/java/com/owncloud/android/db/ProviderMeta.java index b5e9d3e8b636..5953bab1c3b1 100644 --- a/src/main/java/com/owncloud/android/db/ProviderMeta.java +++ b/src/main/java/com/owncloud/android/db/ProviderMeta.java @@ -31,7 +31,7 @@ */ 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 @@ static public class ProviderTableMeta implements BaseColumns { 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 @@ static public class ProviderTableMeta implements BaseColumns { 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"; diff --git a/src/main/java/com/owncloud/android/files/services/FileDownloader.java b/src/main/java/com/owncloud/android/files/services/FileDownloader.java index 9b23473e1c93..017767b98ea8 100644 --- a/src/main/java/com/owncloud/android/files/services/FileDownloader.java +++ b/src/main/java/com/owncloud/android/files/services/FileDownloader.java @@ -43,6 +43,8 @@ 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 int onStartCommand(Intent intent, int flags, int startId) { 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 requestedDownloads = new Vector(); try { DownloadFileOperation newDownload = new DownloadFileOperation(account, file, behaviour, activityName, @@ -634,6 +641,10 @@ private void notifyDownloadResult(DownloadFileOperation download, // 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); diff --git a/src/main/java/com/owncloud/android/files/services/FileUploader.java b/src/main/java/com/owncloud/android/files/services/FileUploader.java index 81cdb81ed350..c958537b578a 100644 --- a/src/main/java/com/owncloud/android/files/services/FileUploader.java +++ b/src/main/java/com/owncloud/android/files/services/FileUploader.java @@ -37,6 +37,7 @@ import android.content.Intent; import android.graphics.BitmapFactory; import android.os.Binder; +import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; @@ -77,11 +78,10 @@ import com.owncloud.android.utils.ThemeUtils; import java.io.File; -import java.util.AbstractList; +import java.util.ArrayList; import java.util.HashMap; -import java.util.Iterator; +import java.util.List; import java.util.Map; -import java.util.Vector; import javax.annotation.Nullable; import javax.inject.Inject; @@ -95,15 +95,14 @@ * * Files to be uploaded are stored persistently using {@link UploadsStorageManager}. * - * On next invocation of {@link FileUploader} uploaded files which - * previously failed will be uploaded again until either upload succeeded or a - * fatal error occurred. + * On next invocation of {@link FileUploader} uploaded files which previously failed will be uploaded again until either + * upload succeeded or a fatal error occurred. * - * Every file passed to this service is uploaded. No filtering is performed. - * However, Intent keys (e.g., KEY_WIFI_ONLY) are obeyed. + * Every file passed to this service is uploaded. No filtering is performed. However, Intent keys (e.g., KEY_WIFI_ONLY) + * are obeyed. */ public class FileUploader extends Service - implements OnDatatransferProgressListener, OnAccountsUpdateListener, UploadFileOperation.OnRenameListener { + implements OnDatatransferProgressListener, OnAccountsUpdateListener, UploadFileOperation.OnRenameListener { private static final String TAG = FileUploader.class.getSimpleName(); @@ -124,10 +123,6 @@ public class FileUploader extends Service public static final String KEY_REMOTE_FILE = "REMOTE_FILE"; public static final String KEY_MIME_TYPE = "MIME_TYPE"; - private Notification mNotification; - - @Inject UserAccountManager accountManager; - /** * Call this Service with only this Intent key if all pending uploads are to be retried. */ @@ -138,8 +133,7 @@ public class FileUploader extends Service // */ // private static final String KEY_RETRY_REMOTE_PATH = "KEY_RETRY_REMOTE_PATH"; /** - * Call this Service with KEY_RETRY and KEY_RETRY_UPLOAD to retry - * upload of file identified by KEY_RETRY_UPLOAD. + * Call this Service with KEY_RETRY and KEY_RETRY_UPLOAD to retry upload of file identified by KEY_RETRY_UPLOAD. */ private static final String KEY_RETRY_UPLOAD = "KEY_RETRY_UPLOAD"; /** @@ -148,9 +142,10 @@ public class FileUploader extends Service public static final String KEY_ACCOUNT = "ACCOUNT"; /** - * Set to true if remote file is to be overwritten. Default action is to upload with different name. + * What {@link NameCollisionPolicy} to do when the file already exists on the remote. */ - public static final String KEY_FORCE_OVERWRITE = "KEY_FORCE_OVERWRITE"; + public static final String KEY_NAME_COLLISION_POLICY = "KEY_NAME_COLLISION_POLICY"; + /** * Set to true if remote folder is to be created if it does not exist. */ @@ -174,13 +169,16 @@ public class FileUploader extends Service public static final int LOCAL_BEHAVIOUR_FORGET = 2; public static final int LOCAL_BEHAVIOUR_DELETE = 3; + + private Notification mNotification; private Looper mServiceLooper; private ServiceHandler mServiceHandler; private IBinder mBinder; private OwnCloudClient mUploadClient; private Account mCurrentAccount; private FileDataStorageManager mStorageManager; - //since there can be only one instance of an Android service, there also just one db connection. + + @Inject UserAccountManager accountManager; @Inject UploadsStorageManager mUploadsStorageManager; @Inject ConnectivityService connectivityService; @Inject PowerManagementService powerManagementService; @@ -188,7 +186,8 @@ public class FileUploader extends Service private IndexedForest mPendingUploads = new IndexedForest<>(); /** - * {@link UploadFileOperation} object of ongoing upload. Can be null. Note: There can only be one concurrent upload! + * {@link UploadFileOperation} object of ongoing upload. Can be null. Note: There can only be one concurrent + * upload! */ private UploadFileOperation mCurrentUpload; @@ -196,17 +195,6 @@ public class FileUploader extends Service private NotificationCompat.Builder mNotificationBuilder; private int mLastPercent; - public static String getUploadsAddedMessage() { - return FileUploader.class.getName() + UPLOADS_ADDED_MESSAGE; - } - - public static String getUploadStartMessage() { - return FileUploader.class.getName() + UPLOAD_START_MESSAGE; - } - - public static String getUploadFinishMessage() { - return FileUploader.class.getName() + UPLOAD_FINISH_MESSAGE; - } @Override public void onRenameUpload() { @@ -214,249 +202,6 @@ public void onRenameUpload() { sendBroadcastUploadStarted(mCurrentUpload); } - /** - * Helper class providing methods to ease requesting commands to {@link FileUploader} . - * - * Avoids the need of checking once and again what extras are needed or optional - * in the {@link Intent} to pass to {@link Context#startService(Intent)}. - */ - public static class UploadRequester { - - /** - * Call to upload several new files - */ - public void uploadNewFile( - Context context, - Account account, - String[] localPaths, - String[] remotePaths, - String[] mimeTypes, - Integer behaviour, - Boolean createRemoteFolder, - int createdBy, - boolean requiresWifi, - boolean requiresCharging - ) { - Intent intent = new Intent(context, FileUploader.class); - - intent.putExtra(FileUploader.KEY_ACCOUNT, account); - intent.putExtra(FileUploader.KEY_LOCAL_FILE, localPaths); - intent.putExtra(FileUploader.KEY_REMOTE_FILE, remotePaths); - intent.putExtra(FileUploader.KEY_MIME_TYPE, mimeTypes); - intent.putExtra(FileUploader.KEY_LOCAL_BEHAVIOUR, behaviour); - intent.putExtra(FileUploader.KEY_CREATE_REMOTE_FOLDER, createRemoteFolder); - intent.putExtra(FileUploader.KEY_CREATED_BY, createdBy); - intent.putExtra(FileUploader.KEY_WHILE_ON_WIFI_ONLY, requiresWifi); - intent.putExtra(FileUploader.KEY_WHILE_CHARGING_ONLY, requiresCharging); - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - context.startForegroundService(intent); - } else { - context.startService(intent); - } - } - - public void uploadFileWithOverwrite( - Context context, - Account account, - String[] localPaths, - String[] remotePaths, - String[] mimeTypes, - Integer behaviour, - Boolean createRemoteFolder, - int createdBy, - boolean requiresWifi, - boolean requiresCharging, - boolean overwrite - ) { - Intent intent = new Intent(context, FileUploader.class); - - intent.putExtra(FileUploader.KEY_ACCOUNT, account); - intent.putExtra(FileUploader.KEY_LOCAL_FILE, localPaths); - intent.putExtra(FileUploader.KEY_REMOTE_FILE, remotePaths); - intent.putExtra(FileUploader.KEY_MIME_TYPE, mimeTypes); - intent.putExtra(FileUploader.KEY_LOCAL_BEHAVIOUR, behaviour); - intent.putExtra(FileUploader.KEY_CREATE_REMOTE_FOLDER, createRemoteFolder); - intent.putExtra(FileUploader.KEY_CREATED_BY, createdBy); - intent.putExtra(FileUploader.KEY_WHILE_ON_WIFI_ONLY, requiresWifi); - intent.putExtra(FileUploader.KEY_WHILE_CHARGING_ONLY, requiresCharging); - intent.putExtra(FileUploader.KEY_FORCE_OVERWRITE, overwrite); - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - context.startForegroundService(intent); - } else { - context.startService(intent); - } - } - - /** - * Call to upload a file - */ - public void uploadFileWithOverwrite(Context context, Account account, String localPath, String remotePath, int - behaviour, String mimeType, boolean createRemoteFile, int createdBy, boolean requiresWifi, - boolean requiresCharging, boolean overwrite) { - - uploadFileWithOverwrite( - context, - account, - new String[]{localPath}, - new String[]{remotePath}, - new String[]{mimeType}, - behaviour, - createRemoteFile, - createdBy, - requiresWifi, - requiresCharging, - overwrite - ); - } - - /** - * Call to upload a new single file - */ - public void uploadNewFile(Context context, Account account, String localPath, String remotePath, int - behaviour, String mimeType, boolean createRemoteFile, int createdBy, boolean requiresWifi, - boolean requiresCharging) { - - uploadNewFile( - context, - account, - new String[]{localPath}, - new String[]{remotePath}, - new String[]{mimeType}, - behaviour, - createRemoteFile, - createdBy, - requiresWifi, - requiresCharging - ); - } - - /** - * Call to update multiple files already uploaded - */ - public void uploadUpdate(Context context, Account account, OCFile[] existingFiles, Integer behaviour, - Boolean forceOverwrite) { - Intent intent = new Intent(context, FileUploader.class); - - intent.putExtra(FileUploader.KEY_ACCOUNT, account); - intent.putExtra(FileUploader.KEY_FILE, existingFiles); - intent.putExtra(FileUploader.KEY_LOCAL_BEHAVIOUR, behaviour); - intent.putExtra(FileUploader.KEY_FORCE_OVERWRITE, forceOverwrite); - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - context.startForegroundService(intent); - } else { - context.startService(intent); - } - } - - /** - * Call to update a dingle file already uploaded - */ - public void uploadUpdate(Context context, Account account, OCFile existingFile, Integer behaviour, - Boolean forceOverwrite) { - - uploadUpdate(context, account, new OCFile[]{existingFile}, behaviour, forceOverwrite); - } - - - /** - * Call to retry upload identified by remotePath - */ - public void retry (Context context, UserAccountManager accountManager, OCUpload upload) { - if (upload != null && context != null) { - Account account = accountManager.getAccountByName(upload.getAccountName()); - retry(context, account, upload); - } else { - throw new IllegalArgumentException("Null parameter!"); - } - } - - private boolean checkIfUploadCanBeRetried(OCUpload ocUpload, boolean gotWifi, boolean isCharging) { - boolean needsWifi = ocUpload.isUseWifiOnly(); - boolean needsCharging = ocUpload.isWhileChargingOnly(); - - return new File(ocUpload.getLocalPath()).exists() && !(needsCharging && !isCharging) && - !(needsWifi && !gotWifi); - - } - - /** - * Retry a subset of all the stored failed uploads. - * - * @param context Caller {@link Context} - * @param account If not null, only failed uploads to this OC account will be retried; otherwise, - * uploads of all accounts will be retried. - * @param uploadResult If not null, only failed uploads with the result specified will be retried; - * otherwise, failed uploads due to any result will be retried. - */ - public void retryFailedUploads( - @NonNull final Context context, - @Nullable final Account account, - @NonNull final UploadsStorageManager uploadsStorageManager, - @NonNull final ConnectivityService connectivityService, - @NonNull final UserAccountManager accountManager, - @NonNull final PowerManagementService powerManagementService, - @Nullable final UploadResult uploadResult - ) { - OCUpload[] failedUploads = uploadsStorageManager.getFailedUploads(); - Account currentAccount = null; - boolean resultMatch; - boolean accountMatch; - - boolean gotNetwork = connectivityService.getActiveNetworkType() != JobRequest.NetworkType.ANY && - !connectivityService.isInternetWalled(); - boolean gotWifi = gotNetwork && Device.getNetworkType(context).equals(JobRequest.NetworkType.UNMETERED); - boolean charging = Device.getBatteryStatus(context).isCharging(); - boolean isPowerSaving = powerManagementService.isPowerSavingEnabled(); - - for ( OCUpload failedUpload: failedUploads) { - accountMatch = account == null || account.name.equals(failedUpload.getAccountName()); - resultMatch = uploadResult == null || uploadResult.equals(failedUpload.getLastResult()); - if (accountMatch && resultMatch) { - if (currentAccount == null || !currentAccount.name.equals(failedUpload.getAccountName())) { - currentAccount = failedUpload.getAccount(accountManager); - } - - if (!new File(failedUpload.getLocalPath()).exists()) { - if (!failedUpload.getLastResult().equals(UploadResult.FILE_NOT_FOUND)) { - failedUpload.setLastResult(UploadResult.FILE_NOT_FOUND); - uploadsStorageManager.updateUpload(failedUpload); - } - } else { - charging = charging || Device.getBatteryStatus(context).getBatteryPercent() == 1; - if (!isPowerSaving && gotNetwork && checkIfUploadCanBeRetried(failedUpload, gotWifi, charging)) { - retry(context, currentAccount, failedUpload); - } - } - } - } - } - - /** - * Private implementation of retry. - * - * @param context - * @param account - * @param upload - */ - private void retry(Context context, Account account, OCUpload upload) { - if (upload != null) { - Intent i = new Intent(context, FileUploader.class); - i.putExtra(FileUploader.KEY_RETRY, true); - i.putExtra(FileUploader.KEY_ACCOUNT, account); - i.putExtra(FileUploader.KEY_RETRY_UPLOAD, upload); - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { - context.startForegroundService(i); - } else { - context.startService(i); - } - } - } - } - /** * Service initialization */ @@ -466,29 +211,27 @@ public void onCreate() { AndroidInjection.inject(this); Log_OC.d(TAG, "Creating service"); mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - HandlerThread thread = new HandlerThread("FileUploaderThread", - Process.THREAD_PRIORITY_BACKGROUND); + HandlerThread thread = new HandlerThread("FileUploaderThread", Process.THREAD_PRIORITY_BACKGROUND); thread.start(); mServiceLooper = thread.getLooper(); mServiceHandler = new ServiceHandler(mServiceLooper, this); mBinder = new FileUploaderBinder(); NotificationCompat.Builder builder = new NotificationCompat.Builder(this).setContentTitle( - getApplicationContext().getResources().getString(R.string.app_name)) - .setContentText(getApplicationContext().getResources().getString(R.string.foreground_service_upload)) - .setSmallIcon(R.drawable.notification_icon) - .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.notification_icon)) - .setColor(ThemeUtils.primaryColor(getApplicationContext(), true)); + getApplicationContext().getResources().getString(R.string.app_name)) + .setContentText(getApplicationContext().getResources().getString(R.string.foreground_service_upload)) + .setSmallIcon(R.drawable.notification_icon) + .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.drawable.notification_icon)) + .setColor(ThemeUtils.primaryColor(getApplicationContext(), true)); - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { builder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_UPLOAD); } mNotification = builder.build(); - int failedCounter = mUploadsStorageManager.failInProgressUploads( - UploadResult.SERVICE_INTERRUPTED // Add UploadResult.KILLED? - ); + // TODO Add UploadResult.KILLED? + int failedCounter = mUploadsStorageManager.failInProgressUploads(UploadResult.SERVICE_INTERRUPTED); if (failedCounter > 0) { resurrection(); } @@ -509,6 +252,7 @@ private void resurrection() { /** * Service clean up */ + @SuppressWarnings("PMD.NullAssignment") @Override public void onDestroy() { Log_OC.v(TAG, "Destroying service"); @@ -528,9 +272,8 @@ public void onDestroy() { /** * Entry point to add one or several files to the queue of uploads. * - * New uploads are added calling to startService(), resulting in a call to - * this method. This ensures the service will keep on working although the - * caller activity goes away. + * New uploads are added calling to startService(), resulting in a call to this method. This ensures the service + * will keep on working although the caller activity goes away. */ @Override public int onStartCommand(Intent intent, int flags, int startId) { @@ -554,201 +297,245 @@ public int onStartCommand(Intent intent, int flags, int startId) { } boolean retry = intent.getBooleanExtra(KEY_RETRY, false); - AbstractList requestedUploads = new Vector<>(); + List requestedUploads = new ArrayList<>(); boolean onWifiOnly = intent.getBooleanExtra(KEY_WHILE_ON_WIFI_ONLY, false); boolean whileChargingOnly = intent.getBooleanExtra(KEY_WHILE_CHARGING_ONLY, false); - if (!retry) { - - if (!(intent.hasExtra(KEY_LOCAL_FILE) || - intent.hasExtra(KEY_FILE))) { + if (!retry) { // Start new uploads + if (!(intent.hasExtra(KEY_LOCAL_FILE) || intent.hasExtra(KEY_FILE))) { Log_OC.e(TAG, "Not enough information provided in intent"); return Service.START_NOT_STICKY; } - String[] localPaths = null; - String[] remotePaths = null; - String[] mimeTypes = null; - OCFile[] files = null; + Integer error = gatherAndStartNewUploads(intent, account, requestedUploads, onWifiOnly, whileChargingOnly); + if (error != null) { + return error; + } + } else { // Retry uploads + if (!intent.hasExtra(KEY_ACCOUNT) || !intent.hasExtra(KEY_RETRY_UPLOAD)) { + Log_OC.e(TAG, "Not enough information provided in intent: no KEY_RETRY_UPLOAD_KEY"); + return START_NOT_STICKY; + } + retryUploads(intent, account, requestedUploads); + } + + if (requestedUploads.size() > 0) { + Message msg = mServiceHandler.obtainMessage(); + msg.arg1 = startId; + msg.obj = requestedUploads; + mServiceHandler.sendMessage(msg); + sendBroadcastUploadsAdded(); + } + return Service.START_NOT_STICKY; + } - if (intent.hasExtra(KEY_FILE)) { - Parcelable[] files_temp = intent.getParcelableArrayExtra(KEY_FILE); - files = new OCFile[files_temp.length]; - System.arraycopy(files_temp, 0, files, 0, files_temp.length); + /** + * Gather and start new uploads. + * + * @return A {@link Service} constant in case of error, {@code null} otherwise. + */ + @Nullable + private Integer gatherAndStartNewUploads( + Intent intent, + Account account, + List requestedUploads, + boolean onWifiOnly, + boolean whileChargingOnly + ) { + String[] localPaths = null; + String[] remotePaths = null; + String[] mimeTypes = null; + OCFile[] files = null; + + if (intent.hasExtra(KEY_FILE)) { + Parcelable[] files_temp = intent.getParcelableArrayExtra(KEY_FILE); + files = new OCFile[files_temp.length]; + System.arraycopy(files_temp, 0, files, 0, files_temp.length); + } else { + localPaths = intent.getStringArrayExtra(KEY_LOCAL_FILE); + remotePaths = intent.getStringArrayExtra(KEY_REMOTE_FILE); + mimeTypes = intent.getStringArrayExtra(KEY_MIME_TYPE); + } - } else { - localPaths = intent.getStringArrayExtra(KEY_LOCAL_FILE); - remotePaths = intent.getStringArrayExtra(KEY_REMOTE_FILE); - mimeTypes = intent.getStringArrayExtra(KEY_MIME_TYPE); + if (intent.hasExtra(KEY_FILE) && files == null) { + Log_OC.e(TAG, "Incorrect array for OCFiles provided in upload intent"); + return Service.START_NOT_STICKY; + } else if (!intent.hasExtra(KEY_FILE)) { + if (localPaths == null) { + Log_OC.e(TAG, "Incorrect array for local paths provided in upload intent"); + return Service.START_NOT_STICKY; } - - if (intent.hasExtra(KEY_FILE) && files == null) { - Log_OC.e(TAG, "Incorrect array for OCFiles provided in upload intent"); + if (remotePaths == null) { + Log_OC.e(TAG, "Incorrect array for remote paths provided in upload intent"); + return Service.START_NOT_STICKY; + } + if (localPaths.length != remotePaths.length) { + Log_OC.e(TAG, "Different number of remote paths and local paths!"); return Service.START_NOT_STICKY; + } - } else if (!intent.hasExtra(KEY_FILE)) { - if (localPaths == null) { - Log_OC.e(TAG, "Incorrect array for local paths provided in upload intent"); - return Service.START_NOT_STICKY; - } - if (remotePaths == null) { - Log_OC.e(TAG, "Incorrect array for remote paths provided in upload intent"); - return Service.START_NOT_STICKY; - } - if (localPaths.length != remotePaths.length) { - Log_OC.e(TAG, "Different number of remote paths and local paths!"); + files = new OCFile[localPaths.length]; + for (int i = 0; i < localPaths.length; i++) { + files[i] = UploadFileOperation.obtainNewOCFileToUpload( + remotePaths[i], + localPaths[i], + mimeTypes != null ? mimeTypes[i] : null + ); + if (files[i] == null) { + Log_OC.e(TAG, "obtainNewOCFileToUpload() returned null for remotePaths[i]:" + remotePaths[i] + + " and localPaths[i]:" + localPaths[i]); return Service.START_NOT_STICKY; } - - files = new OCFile[localPaths.length]; - for (int i = 0; i < localPaths.length; i++) { - files[i] = UploadFileOperation.obtainNewOCFileToUpload( - remotePaths[i], - localPaths[i], - mimeTypes != null ? mimeTypes[i] : null - ); - if (files[i] == null) { - Log_OC.e(TAG, "obtainNewOCFileToUpload() returned null for remotePaths[i]:" + remotePaths[i] - + " and localPaths[i]:" + localPaths[i]); - return Service.START_NOT_STICKY; - } - } } - // at this point variable "OCFile[] files" is loaded correctly. - - boolean forceOverwrite = intent.getBooleanExtra(KEY_FORCE_OVERWRITE, false); - int localAction = intent.getIntExtra(KEY_LOCAL_BEHAVIOUR, LOCAL_BEHAVIOUR_FORGET); - boolean isCreateRemoteFolder = intent.getBooleanExtra(KEY_CREATE_REMOTE_FOLDER, false); - int createdBy = intent.getIntExtra(KEY_CREATED_BY, UploadFileOperation.CREATED_BY_USER); - String uploadKey; - UploadFileOperation newUpload; - try { - for (OCFile file : files) { - - OCUpload ocUpload = new OCUpload(file, account); - ocUpload.setFileSize(file.getFileLength()); - ocUpload.setForceOverwrite(forceOverwrite); - ocUpload.setCreateRemoteFolder(isCreateRemoteFolder); - ocUpload.setCreatedBy(createdBy); - ocUpload.setLocalAction(localAction); - ocUpload.setUseWifiOnly(onWifiOnly); - ocUpload.setWhileChargingOnly(whileChargingOnly); - ocUpload.setUploadStatus(UploadStatus.UPLOAD_IN_PROGRESS); - - - newUpload = new UploadFileOperation( - mUploadsStorageManager, - connectivityService, - powerManagementService, - account, - file, - ocUpload, - forceOverwrite, - localAction, - this, - onWifiOnly, - whileChargingOnly - ); - newUpload.setCreatedBy(createdBy); - if (isCreateRemoteFolder) { - newUpload.setRemoteFolderToBeCreated(); - } - newUpload.addDataTransferProgressListener(this); - newUpload.addDataTransferProgressListener((FileUploaderBinder) mBinder); - - newUpload.addRenameUploadListener(this); - - Pair putResult = mPendingUploads.putIfAbsent( - account.name, - file.getRemotePath(), - newUpload - ); - if (putResult != null) { - uploadKey = putResult.first; - requestedUploads.add(uploadKey); - - // Save upload in database - long id = mUploadsStorageManager.storeUpload(ocUpload); - newUpload.setOCUploadId(id); - } - } + } + // at this point variable "OCFile[] files" is loaded correctly. - } catch (IllegalArgumentException e) { - Log_OC.e(TAG, "Not enough information provided in intent: " + e.getMessage()); - return START_NOT_STICKY; + NameCollisionPolicy nameCollisionPolicy = (NameCollisionPolicy) intent.getSerializableExtra(KEY_NAME_COLLISION_POLICY); + if (nameCollisionPolicy == null) { + nameCollisionPolicy = NameCollisionPolicy.DEFAULT; + } + int localAction = intent.getIntExtra(KEY_LOCAL_BEHAVIOUR, LOCAL_BEHAVIOUR_FORGET); + boolean isCreateRemoteFolder = intent.getBooleanExtra(KEY_CREATE_REMOTE_FOLDER, false); + int createdBy = intent.getIntExtra(KEY_CREATED_BY, UploadFileOperation.CREATED_BY_USER); + try { + for (OCFile file : files) { + startNewUpload( + account, + requestedUploads, + onWifiOnly, + whileChargingOnly, + nameCollisionPolicy, + localAction, + isCreateRemoteFolder, + createdBy, + file + ); + } + } catch (IllegalArgumentException e) { + Log_OC.e(TAG, "Not enough information provided in intent: " + e.getMessage()); + return START_NOT_STICKY; + } catch (IllegalStateException e) { + Log_OC.e(TAG, "Bad information provided in intent: " + e.getMessage()); + return START_NOT_STICKY; + } catch (Exception e) { + Log_OC.e(TAG, "Unexpected exception while processing upload intent", e); + return START_NOT_STICKY; + } + return null; + } - } catch (IllegalStateException e) { - Log_OC.e(TAG, "Bad information provided in intent: " + e.getMessage()); - return START_NOT_STICKY; + /** + * Start a new {@link UploadFileOperation}. + */ + private void startNewUpload( + Account account, + List requestedUploads, + boolean onWifiOnly, + boolean whileChargingOnly, + NameCollisionPolicy nameCollisionPolicy, + int localAction, + boolean isCreateRemoteFolder, + int createdBy, + OCFile file + ) { + OCUpload ocUpload = new OCUpload(file, account); + ocUpload.setFileSize(file.getFileLength()); + ocUpload.setNameCollisionPolicy(nameCollisionPolicy); + ocUpload.setCreateRemoteFolder(isCreateRemoteFolder); + ocUpload.setCreatedBy(createdBy); + ocUpload.setLocalAction(localAction); + ocUpload.setUseWifiOnly(onWifiOnly); + ocUpload.setWhileChargingOnly(whileChargingOnly); + ocUpload.setUploadStatus(UploadStatus.UPLOAD_IN_PROGRESS); + + UploadFileOperation newUpload = new UploadFileOperation( + mUploadsStorageManager, + connectivityService, + powerManagementService, + account, + file, + ocUpload, + nameCollisionPolicy, + localAction, + this, + onWifiOnly, + whileChargingOnly + ); + newUpload.setCreatedBy(createdBy); + if (isCreateRemoteFolder) { + newUpload.setRemoteFolderToBeCreated(); + } + newUpload.addDataTransferProgressListener(this); + newUpload.addDataTransferProgressListener((FileUploaderBinder) mBinder); - } catch (Exception e) { - Log_OC.e(TAG, "Unexpected exception while processing upload intent", e); - return START_NOT_STICKY; + newUpload.addRenameUploadListener(this); - } - // *** TODO REWRITE: block inserted to request A retry; too many code copied, no control exception ***/ - } else { - if (!intent.hasExtra(KEY_ACCOUNT) || !intent.hasExtra(KEY_RETRY_UPLOAD)) { - Log_OC.e(TAG, "Not enough information provided in intent: no KEY_RETRY_UPLOAD_KEY"); - return START_NOT_STICKY; - } - OCUpload upload = intent.getParcelableExtra(KEY_RETRY_UPLOAD); + Pair putResult = mPendingUploads.putIfAbsent( + account.name, + file.getRemotePath(), + newUpload + ); - onWifiOnly = upload.isUseWifiOnly(); - whileChargingOnly = upload.isWhileChargingOnly(); + if (putResult != null) { + requestedUploads.add(putResult.first); - UploadFileOperation newUpload = new UploadFileOperation( - mUploadsStorageManager, - connectivityService, - powerManagementService, - account, - null, - upload, - upload.isForceOverwrite(), // TODO should be read from DB? - upload.getLocalAction(), // TODO should be read from DB? - this, - onWifiOnly, - whileChargingOnly - ); + // Save upload in database + long id = mUploadsStorageManager.storeUpload(ocUpload); + newUpload.setOCUploadId(id); + } + } - newUpload.addDataTransferProgressListener(this); - newUpload.addDataTransferProgressListener((FileUploaderBinder) mBinder); + /** + * Retries a list of uploads. + */ + private void retryUploads(Intent intent, Account account, List requestedUploads) { + boolean onWifiOnly; + boolean whileChargingOnly; + OCUpload upload = intent.getParcelableExtra(KEY_RETRY_UPLOAD); + + onWifiOnly = upload.isUseWifiOnly(); + whileChargingOnly = upload.isWhileChargingOnly(); + + UploadFileOperation newUpload = new UploadFileOperation( + mUploadsStorageManager, + connectivityService, + powerManagementService, + account, + null, + upload, + upload.getNameCollisionPolicy(), + upload.getLocalAction(), + this, + onWifiOnly, + whileChargingOnly + ); - newUpload.addRenameUploadListener(this); + newUpload.addDataTransferProgressListener(this); + newUpload.addDataTransferProgressListener((FileUploaderBinder) mBinder); - Pair putResult = mPendingUploads.putIfAbsent( - account.name, - upload.getRemotePath(), - newUpload - ); - if (putResult != null) { - String uploadKey = putResult.first; - requestedUploads.add(uploadKey); + newUpload.addRenameUploadListener(this); - // Update upload in database - upload.setUploadStatus(UploadStatus.UPLOAD_IN_PROGRESS); - mUploadsStorageManager.updateUpload(upload); - } - } - // *** TODO REWRITE END ***/ + Pair putResult = mPendingUploads.putIfAbsent( + account.name, + upload.getRemotePath(), + newUpload + ); + if (putResult != null) { + String uploadKey = putResult.first; + requestedUploads.add(uploadKey); - if (requestedUploads.size() > 0) { - Message msg = mServiceHandler.obtainMessage(); - msg.arg1 = startId; - msg.obj = requestedUploads; - mServiceHandler.sendMessage(msg); - sendBroadcastUploadsAdded(); + // Update upload in database + upload.setUploadStatus(UploadStatus.UPLOAD_IN_PROGRESS); + mUploadsStorageManager.updateUpload(upload); } - return Service.START_NOT_STICKY; } /** - * Provides a binder object that clients can use to perform operations on - * the queue of uploads, excepting the addition of new files. + * Provides a binder object that clients can use to perform operations on the queue of uploads, excepting the + * addition of new files. * - * Implemented to perform cancellation, pause and resume of existing - * uploads. + * Implemented to perform cancellation, pause and resume of existing uploads. */ @Override public IBinder onBind(Intent intent) { @@ -766,7 +553,7 @@ public boolean onUnbind(Intent intent) { @Override public void onAccountsUpdated(Account[] accounts) { - // Review current upload, and cancel it if its account doen't exist + // Review current upload, and cancel it if its account doesn't exist if (mCurrentUpload != null && !accountManager.exists(mCurrentUpload.getAccount())) { mCurrentUpload.cancel(); } @@ -774,327 +561,41 @@ public void onAccountsUpdated(Account[] accounts) { } /** - * Binder to let client components to perform operations on the queue of uploads. + * Core upload method: sends the file(s) to upload * - * It provides by itself the available operations. + * @param uploadKey Key to access the upload to perform, contained in mPendingUploads */ - public class FileUploaderBinder extends Binder implements OnDatatransferProgressListener { + public void uploadFile(String uploadKey) { + mCurrentUpload = mPendingUploads.get(uploadKey); - /** - * Map of listeners that will be reported about progress of uploads from a - * {@link FileUploaderBinder} instance - */ - private Map mBoundListeners = new HashMap<>(); + if (mCurrentUpload != null) { + /// Check account existence + if (!accountManager.exists(mCurrentUpload.getAccount())) { + Log_OC.w(TAG, "Account " + mCurrentUpload.getAccount().name + + " does not exist anymore -> cancelling all its uploads"); + cancelUploadsForAccount(mCurrentUpload.getAccount()); + return; + } - /** - * Cancels a pending or current upload of a remote file. - * - * @param account ownCloud account where the remote file will be stored. - * @param file A file in the queue of pending uploads - */ - public void cancel(Account account, OCFile file) { - cancel(account.name, file.getRemotePath(), null); - } + /// OK, let's upload + mUploadsStorageManager.updateDatabaseUploadStart(mCurrentUpload); - /** - * Cancels a pending or current upload that was persisted. - * - * @param storedUpload Upload operation persisted - */ - public void cancel(OCUpload storedUpload) { - cancel(storedUpload.getAccountName(), storedUpload.getRemotePath(), null); + notifyUploadStart(mCurrentUpload); - } + sendBroadcastUploadStarted(mCurrentUpload); - /** - * Cancels a pending or current upload of a remote file. - * - * @param accountName Local name of an ownCloud account where the remote file will be stored. - * @param remotePath Remote target of the upload - * - * Setting result code will pause rather than cancel the job - */ - private void cancel(String accountName, String remotePath, @Nullable ResultCode resultCode ) { - Pair removeResult = - mPendingUploads.remove(accountName, remotePath); - UploadFileOperation upload = removeResult.first; - if (upload == null && - mCurrentUpload != null && mCurrentAccount != null && - mCurrentUpload.getRemotePath().startsWith(remotePath) && - accountName.equals(mCurrentAccount.name)) { + RemoteOperationResult uploadResult = null; - upload = mCurrentUpload; - } + try { + /// prepare client object to send the request to the ownCloud server + if (mCurrentAccount == null || !mCurrentAccount.equals(mCurrentUpload.getAccount())) { + mCurrentAccount = mCurrentUpload.getAccount(); + mStorageManager = new FileDataStorageManager(mCurrentAccount, getContentResolver()); + } // else, reuse storage manager from previous operation - if (upload != null) { - upload.cancel(); - // need to update now table in mUploadsStorageManager, - // since the operation will not get to be run by FileUploader#uploadFile - if (resultCode != null) { - mUploadsStorageManager.updateDatabaseUploadResult(new RemoteOperationResult(resultCode), upload); - notifyUploadResult(upload, new RemoteOperationResult(resultCode)); - } else { - mUploadsStorageManager.removeUpload(accountName, remotePath); - } - } - } - - /** - * Cancels all the uploads for an account. - * - * @param account ownCloud account. - */ - public void cancel(Account account) { - Log_OC.d(TAG, "Account= " + account.name); - - if (mCurrentUpload != null) { - Log_OC.d(TAG, "Current Upload Account= " + mCurrentUpload.getAccount().name); - if (mCurrentUpload.getAccount().name.equals(account.name)) { - mCurrentUpload.cancel(); - } - } - // Cancel pending uploads - cancelUploadsForAccount(account); - } - - public void clearListeners() { - mBoundListeners.clear(); - } - - /** - * Returns True when the file described by 'file' is being uploaded to - * the ownCloud account 'account' or waiting for it - * - * If 'file' is a directory, returns 'true' if some of its descendant files - * is uploading or waiting to upload. - * - * Warning: If remote file exists and !forceOverwrite the original file - * is being returned here. That is, it seems as if the original file is - * being updated when actually a new file is being uploaded. - * - * @param account Owncloud account where the remote file will be stored. - * @param file A file that could be in the queue of pending uploads - */ - public boolean isUploading(Account account, OCFile file) { - if (account == null || file == null) { - return false; - } - return mPendingUploads.contains(account.name, file.getRemotePath()); - } - - public boolean isUploadingNow(OCUpload upload) { - return - upload != null && - mCurrentAccount != null && - mCurrentUpload != null && - upload.getAccountName().equals(mCurrentAccount.name) && - upload.getRemotePath().equals(mCurrentUpload.getRemotePath()) - ; - } - - /** - * Adds a listener interested in the progress of the upload for a concrete file. - * - * @param listener Object to notify about progress of transfer. - * @param account ownCloud account holding the file of interest. - * @param file {@link OCFile} of interest for listener. - */ - public void addDatatransferProgressListener( - OnDatatransferProgressListener listener, - Account account, - OCFile file - ) { - if (account == null || file == null || listener == null) { - return; - } - String targetKey = buildRemoteName(account.name, file.getRemotePath()); - mBoundListeners.put(targetKey, listener); - } - - /** - * Adds a listener interested in the progress of the upload for a concrete file. - * - * @param listener Object to notify about progress of transfer. - * @param ocUpload {@link OCUpload} of interest for listener. - */ - public void addDatatransferProgressListener( - OnDatatransferProgressListener listener, - OCUpload ocUpload - ) { - if (ocUpload == null || listener == null) { - return; - } - String targetKey = buildRemoteName(ocUpload.getAccountName(), ocUpload.getRemotePath()); - mBoundListeners.put(targetKey, listener); - } - - /** - * Removes a listener interested in the progress of the upload for a concrete file. - * - * @param listener Object to notify about progress of transfer. - * @param account ownCloud account holding the file of interest. - * @param file {@link OCFile} of interest for listener. - */ - public void removeDatatransferProgressListener( - OnDatatransferProgressListener listener, - Account account, - OCFile file - ) { - if (account == null || file == null || listener == null) { - return; - } - String targetKey = buildRemoteName(account.name, file.getRemotePath()); - if (mBoundListeners.get(targetKey) == listener) { - mBoundListeners.remove(targetKey); - } - } - - /** - * Removes a listener interested in the progress of the upload for a concrete file. - * - * @param listener Object to notify about progress of transfer. - * @param ocUpload Stored upload of interest - */ - public void removeDatatransferProgressListener( - OnDatatransferProgressListener listener, - OCUpload ocUpload - ) { - if (ocUpload == null || listener == null) { - return; - } - String targetKey = buildRemoteName(ocUpload.getAccountName(), ocUpload.getRemotePath()); - if (mBoundListeners.get(targetKey) == listener) { - mBoundListeners.remove(targetKey); - } - } - - @Override - public void onTransferProgress(long progressRate, long totalTransferredSoFar, - long totalToTransfer, String fileName) { - String key = buildRemoteName(mCurrentUpload.getAccount().name, mCurrentUpload.getFile().getRemotePath()); - OnDatatransferProgressListener boundListener = mBoundListeners.get(key); - - if (boundListener != null) { - boundListener.onTransferProgress(progressRate, totalTransferredSoFar, - totalToTransfer, fileName); - } - - if (MainApp.getAppContext() != null) { - if (mCurrentUpload.isWifiRequired() && !Device.getNetworkType(MainApp.getAppContext()). - equals(JobRequest.NetworkType.UNMETERED)) { - cancel(mCurrentUpload.getAccount().name, mCurrentUpload.getFile().getRemotePath() - , ResultCode.DELAYED_FOR_WIFI); - } else if (mCurrentUpload.isChargingRequired() && - !Device.getBatteryStatus(MainApp.getAppContext()).isCharging()) { - cancel(mCurrentUpload.getAccount().name, mCurrentUpload.getFile().getRemotePath() - , ResultCode.DELAYED_FOR_CHARGING); - } else if (!mCurrentUpload.isIgnoringPowerSaveMode() && - powerManagementService.isPowerSavingEnabled()) { - cancel(mCurrentUpload.getAccount().name, mCurrentUpload.getFile().getRemotePath() - , ResultCode.DELAYED_IN_POWER_SAVE_MODE); - } - } - } - - /** - * Builds a key for the map of listeners. - * - * TODO use method in IndexedForest, or refactor both to a common place - * add to local database) to better policy (add to local database, then upload) - * - * @param accountName Local name of the ownCloud account where the file to upload belongs. - * @param remotePath Remote path to upload the file to. - * @return Key - */ - private String buildRemoteName(String accountName, String remotePath) { - return accountName + remotePath; - } - - } - - - /** - * Upload worker. Performs the pending uploads in the order they were - * requested. - * - * Created with the Looper of a new thread, started in - * {@link FileUploader#onCreate()}. - */ - private static class ServiceHandler extends Handler { - // don't make it a final class, and don't remove the static ; lint will - // warn about a possible memory leak - FileUploader mService; - - public ServiceHandler(Looper looper, FileUploader service) { - super(looper); - if (service == null) { - throw new IllegalArgumentException("Received invalid NULL in parameter 'service'"); - } - mService = service; - } - - @Override - public void handleMessage(Message msg) { - @SuppressWarnings("unchecked") - AbstractList requestedUploads = (AbstractList) msg.obj; - if (msg.obj != null) { - Iterator it = requestedUploads.iterator(); - while (it.hasNext()) { - mService.uploadFile(it.next()); - } - } - Log_OC.d(TAG, "Stopping command after id " + msg.arg1); - mService.stopForeground(true); - mService.stopSelf(msg.arg1); - - } - } - - /** - * Core upload method: sends the file(s) to upload - * - * @param uploadKey Key to access the upload to perform, contained in mPendingUploads - */ - public void uploadFile(String uploadKey) { - - mCurrentUpload = mPendingUploads.get(uploadKey); - - if (mCurrentUpload != null) { - - /// Check account existence - if (!accountManager.exists(mCurrentUpload.getAccount())) { - Log_OC.w(TAG, "Account " + mCurrentUpload.getAccount().name + - " does not exist anymore -> cancelling all its uploads"); - cancelUploadsForAccount(mCurrentUpload.getAccount()); - return; - } - - /// OK, let's upload - mUploadsStorageManager.updateDatabaseUploadStart(mCurrentUpload); - - notifyUploadStart(mCurrentUpload); - - sendBroadcastUploadStarted(mCurrentUpload); - - RemoteOperationResult uploadResult = null; - - try { - /// prepare client object to send the request to the ownCloud server - if (mCurrentAccount == null || !mCurrentAccount.equals(mCurrentUpload.getAccount())) { - mCurrentAccount = mCurrentUpload.getAccount(); - mStorageManager = new FileDataStorageManager( - mCurrentAccount, - getContentResolver() - ); - } // else, reuse storage manager from previous operation - - // always get client from client manager, to get fresh credentials in case of update - OwnCloudAccount ocAccount = new OwnCloudAccount( - mCurrentAccount, - this - ); - mUploadClient = OwnCloudClientManagerFactory.getDefaultSingleton(). - getClientFor(ocAccount, this); + // always get client from client manager, to get fresh credentials in case of update + OwnCloudAccount ocAccount = new OwnCloudAccount(mCurrentAccount, this); + mUploadClient = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, this); // // If parent folder is encrypted, upload file encrypted @@ -1106,27 +607,24 @@ public void uploadFile(String uploadKey) { // // uploadResult = uploadEncryptedFileOperation.execute(mUploadClient, mStorageManager); // } else { - /// perform the regular upload - uploadResult = mCurrentUpload.execute(mUploadClient, mStorageManager); + /// perform the regular upload + uploadResult = mCurrentUpload.execute(mUploadClient, mStorageManager); // } - - } catch (Exception e) { Log_OC.e(TAG, "Error uploading", e); uploadResult = new RemoteOperationResult(e); - } finally { Pair removeResult; if (mCurrentUpload.wasRenamed()) { removeResult = mPendingUploads.removePayload( - mCurrentAccount.name, - mCurrentUpload.getOldFile().getRemotePath() + mCurrentAccount.name, + mCurrentUpload.getOldFile().getRemotePath() ); // TODO: grant that name is also updated for mCurrentUpload.getOCUploadId } else { removeResult = mPendingUploads.removePayload(mCurrentAccount.name, - mCurrentUpload.getDecryptedRemotePath()); + mCurrentUpload.getDecryptedRemotePath()); } mUploadsStorageManager.updateDatabaseUploadResult(uploadResult, mCurrentUpload); @@ -1139,7 +637,7 @@ public void uploadFile(String uploadKey) { // generate new Thumbnail final ThumbnailsCacheManager.ThumbnailGenerationTask task = - new ThumbnailsCacheManager.ThumbnailGenerationTask(mStorageManager, mCurrentAccount); + new ThumbnailsCacheManager.ThumbnailGenerationTask(mStorageManager, mCurrentAccount); File file = new File(mCurrentUpload.getOriginalStoragePath()); String remoteId = mCurrentUpload.getFile().getRemoteId(); @@ -1159,16 +657,16 @@ private void notifyUploadStart(UploadFileOperation upload) { mLastPercent = 0; mNotificationBuilder = NotificationUtils.newNotificationBuilder(this); mNotificationBuilder - .setOngoing(true) - .setSmallIcon(R.drawable.notification_icon) - .setTicker(getString(R.string.uploader_upload_in_progress_ticker)) - .setContentTitle(getString(R.string.uploader_upload_in_progress_ticker)) - .setProgress(100, 0, false) - .setContentText( - String.format(getString(R.string.uploader_upload_in_progress_content), 0, upload.getFileName()) - ); + .setOngoing(true) + .setSmallIcon(R.drawable.notification_icon) + .setTicker(getString(R.string.uploader_upload_in_progress_ticker)) + .setContentTitle(getString(R.string.uploader_upload_in_progress_ticker)) + .setProgress(100, 0, false) + .setContentText( + String.format(getString(R.string.uploader_upload_in_progress_content), 0, upload.getFileName()) + ); - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { mNotificationBuilder.setChannelId(NotificationUtils.NOTIFICATION_CHANNEL_UPLOAD); } @@ -1178,7 +676,7 @@ private void notifyUploadStart(UploadFileOperation upload) { showUploadListIntent.putExtra(FileActivity.EXTRA_ACCOUNT, upload.getAccount()); showUploadListIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); mNotificationBuilder.setContentIntent(PendingIntent.getActivity(this, (int) System.currentTimeMillis(), - showUploadListIntent, 0)); + showUploadListIntent, 0)); if (!upload.isInstantPicture() && !upload.isInstantVideo()) { if (mNotificationManager == null) { @@ -1195,8 +693,12 @@ private void notifyUploadStart(UploadFileOperation upload) { * Callback method to update the progress bar in the status notification */ @Override - public void onTransferProgress(long progressRate, long totalTransferredSoFar, - long totalToTransfer, String filePath) { + public void onTransferProgress( + long progressRate, + long totalTransferredSoFar, + long totalToTransfer, + String filePath + ) { int percent = (int) (100.0 * ((double) totalTransferredSoFar) / ((double) totalToTransfer)); if (percent != mLastPercent) { mNotificationBuilder.setProgress(100, percent, false); @@ -1214,8 +716,7 @@ public void onTransferProgress(long progressRate, long totalTransferredSoFar, * @param uploadResult Result of the upload operation. * @param upload Finished upload operation */ - private void notifyUploadResult(UploadFileOperation upload, - RemoteOperationResult uploadResult) { + private void notifyUploadResult(UploadFileOperation upload, RemoteOperationResult uploadResult) { Log_OC.d(TAG, "NotifyUploadResult with resultCode: " + uploadResult.getCode()); // cancelled operation or success -> silent removal of progress notification if (mNotificationManager == null) { @@ -1231,22 +732,26 @@ private void notifyUploadResult(UploadFileOperation upload, !uploadResult.getCode().equals(ResultCode.DELAYED_FOR_WIFI) && !uploadResult.getCode().equals(ResultCode.DELAYED_FOR_CHARGING) && !uploadResult.getCode().equals(ResultCode.DELAYED_IN_POWER_SAVE_MODE) && - !uploadResult.getCode().equals(ResultCode.LOCK_FAILED) ) { + !uploadResult.getCode().equals(ResultCode.LOCK_FAILED)) { int tickerId = R.string.uploader_upload_failed_ticker; String content; // check credentials error - boolean needsToUpdateCredentials = ResultCode.UNAUTHORIZED.equals(uploadResult.getCode()); - tickerId = needsToUpdateCredentials ? R.string.uploader_upload_failed_credentials_error : tickerId; + boolean needsToUpdateCredentials = uploadResult.getCode() == ResultCode.UNAUTHORIZED; + if (needsToUpdateCredentials) { + tickerId = R.string.uploader_upload_failed_credentials_error; + } else if (uploadResult.getCode() == ResultCode.SYNC_CONFLICT) { // check file conflict + tickerId = R.string.uploader_upload_failed_sync_conflict_error; + } mNotificationBuilder - .setTicker(getString(tickerId)) - .setContentTitle(getString(tickerId)) - .setAutoCancel(true) - .setOngoing(false) - .setProgress(0, 0, false); + .setTicker(getString(tickerId)) + .setContentTitle(getString(tickerId)) + .setAutoCancel(true) + .setOngoing(false) + .setProgress(0, 0, false); content = ErrorMessageAdapter.getErrorCauseMessage(uploadResult, upload, getResources()); @@ -1254,31 +759,31 @@ private void notifyUploadResult(UploadFileOperation upload, // let the user update credentials with one click Intent updateAccountCredentials = new Intent(this, AuthenticatorActivity.class); updateAccountCredentials.putExtra( - AuthenticatorActivity.EXTRA_ACCOUNT, upload.getAccount() + AuthenticatorActivity.EXTRA_ACCOUNT, upload.getAccount() ); updateAccountCredentials.putExtra( - AuthenticatorActivity.EXTRA_ACTION, - AuthenticatorActivity.ACTION_UPDATE_EXPIRED_TOKEN + AuthenticatorActivity.EXTRA_ACTION, + AuthenticatorActivity.ACTION_UPDATE_EXPIRED_TOKEN ); updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); updateAccountCredentials.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS); updateAccountCredentials.addFlags(Intent.FLAG_FROM_BACKGROUND); mNotificationBuilder.setContentIntent(PendingIntent.getActivity( - this, - (int) System.currentTimeMillis(), - updateAccountCredentials, - PendingIntent.FLAG_ONE_SHOT + this, + (int) System.currentTimeMillis(), + updateAccountCredentials, + PendingIntent.FLAG_ONE_SHOT )); - } else { //in case of failure, do not show details file view (because there is no file!) Intent showUploadListIntent = new Intent(this, UploadListActivity.class); showUploadListIntent.putExtra(FileActivity.EXTRA_FILE, upload.getFile()); showUploadListIntent.putExtra(FileActivity.EXTRA_ACCOUNT, upload.getAccount()); showUploadListIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - mNotificationBuilder.setContentIntent(PendingIntent.getActivity(this, (int) System.currentTimeMillis(), - showUploadListIntent, 0)); + mNotificationBuilder.setContentIntent(PendingIntent.getActivity( + this, (int) System.currentTimeMillis(), showUploadListIntent, 0 + )); } mNotificationBuilder.setContentText(content); @@ -1287,8 +792,7 @@ private void notifyUploadResult(UploadFileOperation upload, } /** - * Sends a broadcast in order to the interested activities can update their - * view + * Sends a broadcast in order to the interested activities can update their view * * TODO - no more broadcasts, replace with a callback to subscribed listeners */ @@ -1300,16 +804,13 @@ private void sendBroadcastUploadsAdded() { } /** - * Sends a broadcast in order to the interested activities can update their - * view + * Sends a broadcast in order to the interested activities can update their view * * TODO - no more broadcasts, replace with a callback to subscribed listeners * * @param upload Finished upload operation */ - private void sendBroadcastUploadStarted( - UploadFileOperation upload) { - + private void sendBroadcastUploadStarted(UploadFileOperation upload) { Intent start = new Intent(getUploadStartMessage()); start.putExtra(EXTRA_REMOTE_PATH, upload.getRemotePath()); // real remote start.putExtra(EXTRA_OLD_FILE_PATH, upload.getOriginalStoragePath()); @@ -1320,8 +821,7 @@ private void sendBroadcastUploadStarted( } /** - * Sends a broadcast in order to the interested activities can update their - * view + * Sends a broadcast in order to the interested activities can update their view * * TODO - no more broadcasts, replace with a callback to subscribed listeners * @@ -1330,10 +830,10 @@ private void sendBroadcastUploadStarted( * @param unlinkedFromRemotePath Path in the uploads tree where the upload was unlinked from */ private void sendBroadcastUploadFinished( - UploadFileOperation upload, - RemoteOperationResult uploadResult, - String unlinkedFromRemotePath) { - + UploadFileOperation upload, + RemoteOperationResult uploadResult, + String unlinkedFromRemotePath + ) { Intent end = new Intent(getUploadFinishMessage()); end.putExtra(EXTRA_REMOTE_PATH, upload.getRemotePath()); // real remote // path, after @@ -1356,10 +856,500 @@ private void sendBroadcastUploadFinished( /** * Remove and 'forgets' pending uploads of an account. * - * @param account Account which uploads will be cancelled + * @param account Account which uploads will be cancelled */ private void cancelUploadsForAccount(Account account) { mPendingUploads.remove(account.name); mUploadsStorageManager.removeUploads(account.name); } + + + /** + * Upload a new file + */ + public static void uploadNewFile( + Context context, + Account account, + String localPath, + String remotePath, + int behaviour, + String mimeType, + boolean createRemoteFile, + int createdBy, + boolean requiresWifi, + boolean requiresCharging, + NameCollisionPolicy nameCollisionPolicy + ) { + uploadNewFile( + context, + account, + new String[]{localPath}, + new String[]{remotePath}, + new String[]{mimeType}, + behaviour, + createRemoteFile, + createdBy, + requiresWifi, + requiresCharging, + nameCollisionPolicy + ); + } + + /** + * Upload multiple new files + */ + public static void uploadNewFile( + Context context, + Account account, + String[] localPaths, + String[] remotePaths, + String[] mimeTypes, + Integer behaviour, + Boolean createRemoteFolder, + int createdBy, + boolean requiresWifi, + boolean requiresCharging, + NameCollisionPolicy nameCollisionPolicy + ) { + Intent intent = new Intent(context, FileUploader.class); + + intent.putExtra(FileUploader.KEY_ACCOUNT, account); + intent.putExtra(FileUploader.KEY_LOCAL_FILE, localPaths); + intent.putExtra(FileUploader.KEY_REMOTE_FILE, remotePaths); + intent.putExtra(FileUploader.KEY_MIME_TYPE, mimeTypes); + intent.putExtra(FileUploader.KEY_LOCAL_BEHAVIOUR, behaviour); + intent.putExtra(FileUploader.KEY_CREATE_REMOTE_FOLDER, createRemoteFolder); + intent.putExtra(FileUploader.KEY_CREATED_BY, createdBy); + intent.putExtra(FileUploader.KEY_WHILE_ON_WIFI_ONLY, requiresWifi); + intent.putExtra(FileUploader.KEY_WHILE_CHARGING_ONLY, requiresCharging); + intent.putExtra(FileUploader.KEY_NAME_COLLISION_POLICY, nameCollisionPolicy); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent); + } else { + context.startService(intent); + } + } + + /** + * Upload and overwrite an already uploaded file + */ + public static void uploadUpdateFile( + Context context, + Account account, + OCFile existingFile, + Integer behaviour, + NameCollisionPolicy nameCollisionPolicy + ) { + uploadUpdateFile(context, account, new OCFile[]{existingFile}, behaviour, nameCollisionPolicy); + } + + /** + * Upload and overwrite already uploaded files + */ + public static void uploadUpdateFile( + Context context, + Account account, + OCFile[] existingFiles, + Integer behaviour, + NameCollisionPolicy nameCollisionPolicy + ) { + Intent intent = new Intent(context, FileUploader.class); + + intent.putExtra(FileUploader.KEY_ACCOUNT, account); + intent.putExtra(FileUploader.KEY_FILE, existingFiles); + intent.putExtra(FileUploader.KEY_LOCAL_BEHAVIOUR, behaviour); + intent.putExtra(FileUploader.KEY_NAME_COLLISION_POLICY, nameCollisionPolicy); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(intent); + } else { + context.startService(intent); + } + } + + /** + * Retry a failed {@link OCUpload} identified by {@link OCUpload#getRemotePath()} + */ + public static void retryUpload(@NonNull Context context, @NonNull Account account, @NonNull OCUpload upload) { + Intent i = new Intent(context, FileUploader.class); + i.putExtra(FileUploader.KEY_RETRY, true); + i.putExtra(FileUploader.KEY_ACCOUNT, account); + i.putExtra(FileUploader.KEY_RETRY_UPLOAD, upload); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + context.startForegroundService(i); + } else { + context.startService(i); + } + } + + /** + * Retry a subset of all the stored failed uploads. + * + * @param context Caller {@link Context} + * @param account If not null, only failed uploads to this OC account will be retried; otherwise, uploads of + * all accounts will be retried. + * @param uploadResult If not null, only failed uploads with the result specified will be retried; otherwise, failed + * uploads due to any result will be retried. + */ + public static void retryFailedUploads( + @NonNull final Context context, + @Nullable final Account account, + @NonNull final UploadsStorageManager uploadsStorageManager, + @NonNull final ConnectivityService connectivityService, + @NonNull final UserAccountManager accountManager, + @NonNull final PowerManagementService powerManagementService, + @Nullable final UploadResult uploadResult + ) { + OCUpload[] failedUploads = uploadsStorageManager.getFailedUploads(); + Account currentAccount = null; + boolean resultMatch; + boolean accountMatch; + + boolean gotNetwork = connectivityService.getActiveNetworkType() != JobRequest.NetworkType.ANY && + !connectivityService.isInternetWalled(); + boolean gotWifi = gotNetwork && Device.getNetworkType(context).equals(JobRequest.NetworkType.UNMETERED); + boolean charging = Device.getBatteryStatus(context).isCharging(); + boolean isPowerSaving = powerManagementService.isPowerSavingEnabled(); + + for (OCUpload failedUpload : failedUploads) { + accountMatch = account == null || account.name.equals(failedUpload.getAccountName()); + resultMatch = uploadResult == null || uploadResult == failedUpload.getLastResult(); + if (accountMatch && resultMatch) { + if (currentAccount == null || !currentAccount.name.equals(failedUpload.getAccountName())) { + currentAccount = failedUpload.getAccount(accountManager); + } + + if (!new File(failedUpload.getLocalPath()).exists()) { + if (failedUpload.getLastResult() != UploadResult.FILE_NOT_FOUND) { + failedUpload.setLastResult(UploadResult.FILE_NOT_FOUND); + uploadsStorageManager.updateUpload(failedUpload); + } + } else { + charging = charging || Device.getBatteryStatus(context).getBatteryPercent() == 1; + if (!isPowerSaving && gotNetwork && canUploadBeRetried(failedUpload, gotWifi, charging)) { + retryUpload(context, currentAccount, failedUpload); + } + } + } + } + } + + private static boolean canUploadBeRetried(OCUpload upload, boolean gotWifi, boolean isCharging) { + File file = new File(upload.getLocalPath()); + boolean needsWifi = upload.isUseWifiOnly(); + boolean needsCharging = upload.isWhileChargingOnly(); + + return file.exists() && (!needsWifi || gotWifi) && (!needsCharging || isCharging); + } + + public static String getUploadsAddedMessage() { + return FileUploader.class.getName() + UPLOADS_ADDED_MESSAGE; + } + + public static String getUploadStartMessage() { + return FileUploader.class.getName() + UPLOAD_START_MESSAGE; + } + + public static String getUploadFinishMessage() { + return FileUploader.class.getName() + UPLOAD_FINISH_MESSAGE; + } + + + /** + * Ordinal of enumerated constants is important for old data compatibility. + */ + public enum NameCollisionPolicy { + RENAME, // Ordinal corresponds to old forceOverwrite = false (0 in database) + OVERWRITE, // Ordinal corresponds to old forceOverwrite = true (1 in database) + CANCEL, + ASK_USER; + + public static final NameCollisionPolicy DEFAULT = RENAME; + + public static NameCollisionPolicy deserialize(int ordinal) { + NameCollisionPolicy[] values = NameCollisionPolicy.values(); + return ordinal >= 0 && ordinal < values.length ? values[ordinal] : DEFAULT; + } + + public int serialize() { + return this.ordinal(); + } + } + + /** + * Binder to let client components to perform operations on the queue of uploads. + * + * It provides by itself the available operations. + */ + public class FileUploaderBinder extends Binder implements OnDatatransferProgressListener { + + /** + * Map of listeners that will be reported about progress of uploads from a {@link FileUploaderBinder} instance + */ + private Map mBoundListeners = new HashMap<>(); + + /** + * Cancels a pending or current upload of a remote file. + * + * @param account ownCloud account where the remote file will be stored. + * @param file A file in the queue of pending uploads + */ + public void cancel(Account account, OCFile file) { + cancel(account.name, file.getRemotePath(), null); + } + + /** + * Cancels a pending or current upload that was persisted. + * + * @param storedUpload Upload operation persisted + */ + public void cancel(OCUpload storedUpload) { + cancel(storedUpload.getAccountName(), storedUpload.getRemotePath(), null); + } + + /** + * Cancels a pending or current upload of a remote file. + * + * @param accountName Local name of an ownCloud account where the remote file will be stored. + * @param remotePath Remote target of the upload + * @param resultCode Setting result code will pause rather than cancel the job + */ + private void cancel(String accountName, String remotePath, @Nullable ResultCode resultCode) { + Pair removeResult = mPendingUploads.remove(accountName, remotePath); + UploadFileOperation upload = removeResult.first; + if (upload == null && mCurrentUpload != null && mCurrentAccount != null && + mCurrentUpload.getRemotePath().startsWith(remotePath) && accountName.equals(mCurrentAccount.name)) { + + upload = mCurrentUpload; + } + + if (upload != null) { + upload.cancel(); + // need to update now table in mUploadsStorageManager, + // since the operation will not get to be run by FileUploader#uploadFile + if (resultCode != null) { + mUploadsStorageManager.updateDatabaseUploadResult(new RemoteOperationResult(resultCode), upload); + notifyUploadResult(upload, new RemoteOperationResult(resultCode)); + } else { + mUploadsStorageManager.removeUpload(accountName, remotePath); + } + } + } + + /** + * Cancels all the uploads for an account. + * + * @param account ownCloud account. + */ + public void cancel(Account account) { + Log_OC.d(TAG, "Account= " + account.name); + + if (mCurrentUpload != null) { + Log_OC.d(TAG, "Current Upload Account= " + mCurrentUpload.getAccount().name); + if (mCurrentUpload.getAccount().name.equals(account.name)) { + mCurrentUpload.cancel(); + } + } + + // Cancel pending uploads + cancelUploadsForAccount(account); + } + + public void clearListeners() { + mBoundListeners.clear(); + } + + /** + * Returns True when the file described by 'file' is being uploaded to the ownCloud account 'account' or waiting + * for it + * + * If 'file' is a directory, returns 'true' if some of its descendant files is uploading or waiting to upload. + * + * Warning: If remote file exists and target was renamed the original file is being returned here. That is, it + * seems as if the original file is being updated when actually a new file is being uploaded. + * + * @param account Owncloud account where the remote file will be stored. + * @param file A file that could be in the queue of pending uploads + */ + public boolean isUploading(Account account, OCFile file) { + if (account == null || file == null) { + return false; + } + + return mPendingUploads.contains(account.name, file.getRemotePath()); + } + + public boolean isUploadingNow(OCUpload upload) { + return upload != null && + mCurrentAccount != null && + mCurrentUpload != null && + upload.getAccountName().equals(mCurrentAccount.name) && + upload.getRemotePath().equals(mCurrentUpload.getRemotePath()); + } + + /** + * Adds a listener interested in the progress of the upload for a concrete file. + * + * @param listener Object to notify about progress of transfer. + * @param account ownCloud account holding the file of interest. + * @param file {@link OCFile} of interest for listener. + */ + public void addDatatransferProgressListener( + OnDatatransferProgressListener listener, + Account account, + OCFile file + ) { + if (account == null || file == null || listener == null) { + return; + } + + String targetKey = buildRemoteName(account.name, file.getRemotePath()); + mBoundListeners.put(targetKey, listener); + } + + /** + * Adds a listener interested in the progress of the upload for a concrete file. + * + * @param listener Object to notify about progress of transfer. + * @param ocUpload {@link OCUpload} of interest for listener. + */ + public void addDatatransferProgressListener( + OnDatatransferProgressListener listener, + OCUpload ocUpload + ) { + if (ocUpload == null || listener == null) { + return; + } + + String targetKey = buildRemoteName(ocUpload.getAccountName(), ocUpload.getRemotePath()); + mBoundListeners.put(targetKey, listener); + } + + /** + * Removes a listener interested in the progress of the upload for a concrete file. + * + * @param listener Object to notify about progress of transfer. + * @param account ownCloud account holding the file of interest. + * @param file {@link OCFile} of interest for listener. + */ + public void removeDatatransferProgressListener( + OnDatatransferProgressListener listener, + Account account, + OCFile file + ) { + if (account == null || file == null || listener == null) { + return; + } + + String targetKey = buildRemoteName(account.name, file.getRemotePath()); + if (mBoundListeners.get(targetKey) == listener) { + mBoundListeners.remove(targetKey); + } + } + + /** + * Removes a listener interested in the progress of the upload for a concrete file. + * + * @param listener Object to notify about progress of transfer. + * @param ocUpload Stored upload of interest + */ + public void removeDatatransferProgressListener( + OnDatatransferProgressListener listener, + OCUpload ocUpload + ) { + if (ocUpload == null || listener == null) { + return; + } + + String targetKey = buildRemoteName(ocUpload.getAccountName(), ocUpload.getRemotePath()); + if (mBoundListeners.get(targetKey) == listener) { + mBoundListeners.remove(targetKey); + } + } + + @Override + public void onTransferProgress( + long progressRate, + long totalTransferredSoFar, + long totalToTransfer, + String fileName + ) { + String key = buildRemoteName(mCurrentUpload.getAccount().name, mCurrentUpload.getFile().getRemotePath()); + OnDatatransferProgressListener boundListener = mBoundListeners.get(key); + + if (boundListener != null) { + boundListener.onTransferProgress(progressRate, totalTransferredSoFar, totalToTransfer, fileName); + } + + Context context = MainApp.getAppContext(); + if (context != null) { + ResultCode cancelReason = null; + if (mCurrentUpload.isWifiRequired() && !Device.getNetworkType(context).equals(JobRequest.NetworkType.UNMETERED)) { + cancelReason = ResultCode.DELAYED_FOR_WIFI; + } else if (mCurrentUpload.isChargingRequired() && !Device.getBatteryStatus(context).isCharging()) { + cancelReason = ResultCode.DELAYED_FOR_CHARGING; + } else if (!mCurrentUpload.isIgnoringPowerSaveMode() && powerManagementService.isPowerSavingEnabled()) { + cancelReason = ResultCode.DELAYED_IN_POWER_SAVE_MODE; + } + + if (cancelReason != null) { + cancel( + mCurrentUpload.getAccount().name, + mCurrentUpload.getFile().getRemotePath(), + cancelReason + ); + } + } + } + + /** + * Builds a key for the map of listeners. + * + * TODO use method in IndexedForest, or refactor both to a common place add to local database) to better policy + * (add to local database, then upload) + * + * @param accountName Local name of the ownCloud account where the file to upload belongs. + * @param remotePath Remote path to upload the file to. + * @return Key + */ + private String buildRemoteName(String accountName, String remotePath) { + return accountName + remotePath; + } + } + + + /** + * Upload worker. Performs the pending uploads in the order they were requested. + * + * Created with the Looper of a new thread, started in {@link FileUploader#onCreate()}. + */ + private static class ServiceHandler extends Handler { + // don't make it a final class, and don't remove the static ; lint will + // warn about a possible memory leak + private FileUploader mService; + + public ServiceHandler(Looper looper, FileUploader service) { + super(looper); + if (service == null) { + throw new IllegalArgumentException("Received invalid NULL in parameter 'service'"); + } + mService = service; + } + + @Override + public void handleMessage(Message msg) { + @SuppressWarnings("unchecked") + List requestedUploads = (List) msg.obj; + if (msg.obj != null) { + for (String requestedUpload : requestedUploads) { + mService.uploadFile(requestedUpload); + } + } + Log_OC.d(TAG, "Stopping command after id " + msg.arg1); + mService.stopForeground(true); + mService.stopSelf(msg.arg1); + } + } } diff --git a/src/main/java/com/owncloud/android/jobs/ContactsBackupJob.java b/src/main/java/com/owncloud/android/jobs/ContactsBackupJob.java index 1036be620c90..67918cfaaca5 100644 --- a/src/main/java/com/owncloud/android/jobs/ContactsBackupJob.java +++ b/src/main/java/com/owncloud/android/jobs/ContactsBackupJob.java @@ -169,18 +169,18 @@ private void backupContact(User user, String backupFolder) { } } - 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 ); } diff --git a/src/main/java/com/owncloud/android/jobs/FilesSyncJob.java b/src/main/java/com/owncloud/android/jobs/FilesSyncJob.java index 0268bec03813..f507fa224470 100644 --- a/src/main/java/com/owncloud/android/jobs/FilesSyncJob.java +++ b/src/main/java/com/owncloud/android/jobs/FilesSyncJob.java @@ -131,7 +131,7 @@ protected Result onRunJob(@NonNull Params params) { 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 @@ protected Result onRunJob(@NonNull Params params) { 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 @@ protected Result onRunJob(@NonNull Params params) { 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 @@ private void syncFolder(Context context, Resources resources, boolean lightVersi 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, diff --git a/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java b/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java index 72e4a1ecbe61..d2ed15ccdd45 100644 --- a/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java +++ b/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java @@ -291,8 +291,13 @@ protected RemoteOperationResult run(OwnCloudClient client) { * @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; } diff --git a/src/main/java/com/owncloud/android/operations/UploadFileOperation.java b/src/main/java/com/owncloud/android/operations/UploadFileOperation.java index cf2d893ebb28..20fd02d8b636 100644 --- a/src/main/java/com/owncloud/android/operations/UploadFileOperation.java +++ b/src/main/java/com/owncloud/android/operations/UploadFileOperation.java @@ -90,6 +90,7 @@ 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 UploadFileOperation(UploadsStorageManager uploadsStorageManager, Account account, OCFile file, OCUpload upload, - boolean forceOverwrite, + FileUploader.NameCollisionPolicy nameCollisionPolicy, int localBehaviour, Context context, boolean onWifiOnly, @@ -218,7 +219,7 @@ public UploadFileOperation(UploadsStorageManager uploadsStorageManager, mOnWifiOnly = onWifiOnly; mWhileChargingOnly = whileChargingOnly; mRemotePath = upload.getRemotePath(); - mForceOverwrite = forceOverwrite; + mNameCollisionPolicy = nameCollisionPolicy; mLocalBehaviour = localBehaviour; mOriginalStoragePath = mFile.getStoragePath(); mContext = context; @@ -504,7 +505,11 @@ private RemoteOperationResult encryptedUpload(OwnCloudClient client, OCFile pare /**** 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 @@ private RemoteOperationResult normalUpload(OwnCloudClient client) { } // 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 @@ private RemoteOperationResult copyFile(File originalFile, String expectedPath) t 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 @@ private OCFile createLocalFolder(String remotePath) { /** - * 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 @@ private void createNewOCFile(String newRemotePath) { } /** - * 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, diff --git a/src/main/java/com/owncloud/android/providers/FileContentProvider.java b/src/main/java/com/owncloud/android/providers/FileContentProvider.java index 583d7576200c..3fd9a3af48da 100644 --- a/src/main/java/com/owncloud/android/providers/FileContentProvider.java +++ b/src/main/java/com/owncloud/android/providers/FileContentProvider.java @@ -804,7 +804,7 @@ private void createUploadsTable(SQLiteDatabase db) { + 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 @@ private void createSyncedFoldersTable(SQLiteDatabase db) { + 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 void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 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)); + } } } } diff --git a/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.java b/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.java index 7733900af6c3..9180a86f4348 100644 --- a/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.java +++ b/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.java @@ -25,6 +25,8 @@ 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.Decision; import com.owncloud.android.ui.dialog.ConflictsResolveDialog.OnConflictDecisionMadeListener; +import javax.inject.Inject; + /** * Wrapper activity which will be launched if keep-in-sync file will be modified by external * application. */ public class ConflictsResolveActivity extends FileActivity implements OnConflictDecisionMadeListener { + /** + * A nullable upload entry that must be removed when and if the conflict is resolved. + */ + public static final String EXTRA_CONFLICT_UPLOAD = "CONFLICT_UPLOAD"; + /** + * Specify the upload local behaviour when there is no CONFLICT_UPLOAD. + */ + public static final String EXTRA_LOCAL_BEHAVIOUR = "LOCAL_BEHAVIOUR"; private static final String TAG = ConflictsResolveActivity.class.getSimpleName(); + @Inject UploadsStorageManager uploadsStorageManager; + + private OCUpload conflictUpload; + private int localBehaviour = FileUploader.LOCAL_BEHAVIOUR_FORGET; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + + if (savedInstanceState != null) { + conflictUpload = savedInstanceState.getParcelable(EXTRA_CONFLICT_UPLOAD); + localBehaviour = savedInstanceState.getInt(EXTRA_LOCAL_BEHAVIOUR); + } else { + conflictUpload = getIntent().getParcelableExtra(EXTRA_CONFLICT_UPLOAD); + localBehaviour = getIntent().getIntExtra(EXTRA_LOCAL_BEHAVIOUR, localBehaviour); + } + + if (conflictUpload != null) { + localBehaviour = conflictUpload.getLocalAction(); + } } @Override public void conflictDecisionMade(Decision decision) { + if (decision == Decision.CANCEL) { + return; + } - Integer behaviour = null; - Boolean forceOverwrite = null; + OCFile file = getFile(); switch (decision) { - case CANCEL: - finish(); - return; - case OVERWRITE: - // use local version -> overwrite on server - forceOverwrite = true; + case KEEP_LOCAL: // Upload + FileUploader.uploadUpdateFile( + this, + getAccount(), + file, + localBehaviour, + FileUploader.NameCollisionPolicy.OVERWRITE + ); + + if (conflictUpload != null) { + uploadsStorageManager.removeUpload(conflictUpload); + } break; - case KEEP_BOTH: - behaviour = FileUploader.LOCAL_BEHAVIOUR_MOVE; + case KEEP_BOTH: // Upload + FileUploader.uploadUpdateFile( + this, + getAccount(), + file, + localBehaviour, + FileUploader.NameCollisionPolicy.RENAME + ); + + if (conflictUpload != null) { + uploadsStorageManager.removeUpload(conflictUpload); + } + break; + case KEEP_SERVER: // Download + if (!this.shouldDeleteLocal()) { + // Overwrite local file + Intent intent = new Intent(this, FileDownloader.class); + intent.putExtra(FileDownloader.EXTRA_ACCOUNT, getAccount()); + intent.putExtra(FileDownloader.EXTRA_FILE, file); + if (conflictUpload != null) { + intent.putExtra(FileDownloader.EXTRA_CONFLICT_UPLOAD, conflictUpload); + } + startService(intent); + } break; - case SERVER: - // use server version -> delete local, request download - Intent intent = new Intent(this, FileDownloader.class); - intent.putExtra(FileDownloader.EXTRA_ACCOUNT, getAccount()); - intent.putExtra(FileDownloader.EXTRA_FILE, getFile()); - startService(intent); - finish(); - return; - default: - Log_OC.e(TAG, "Unhandled conflict decision " + decision); - return; } - FileUploader.UploadRequester requester = new FileUploader.UploadRequester(); - requester.uploadUpdate(this, getAccount(), getFile(), behaviour, forceOverwrite); finish(); } @@ -87,26 +133,27 @@ protected void onStart() { 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; } } diff --git a/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java b/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java index bcc2dbc5905c..7815fa40e2e4 100644 --- a/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java +++ b/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.java @@ -1043,8 +1043,7 @@ private void requestUploadOfFilesFromFileSystem(String[] filePaths, int resultCo break; } - FileUploader.UploadRequester requester = new FileUploader.UploadRequester(); - requester.uploadNewFile( + FileUploader.uploadNewFile( this, getAccount(), filePaths, @@ -1054,7 +1053,8 @@ private void requestUploadOfFilesFromFileSystem(String[] filePaths, int resultCo false, // do not create parent folder if not existent UploadFileOperation.CREATED_BY_USER, false, - false + false, + FileUploader.NameCollisionPolicy.ASK_USER ); } else { diff --git a/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java b/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java index 35536bad1054..034790ea2c0d 100755 --- a/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java +++ b/src/main/java/com/owncloud/android/ui/activity/ReceiveExternalFilesActivity.java @@ -888,19 +888,19 @@ private boolean somethingToUpload() { } 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(); } diff --git a/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.java b/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.java index 6eb7b53f5ac6..62e3936fe7d6 100644 --- a/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.java +++ b/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.java @@ -416,6 +416,7 @@ private SyncedFolderDisplayItem createSyncedFolderWithoutMediaFolder(@NonNull Sy syncedFolder.getRemotePath(), syncedFolder.isWifiOnly(), syncedFolder.isChargingOnly(), + syncedFolder.isExisting(), syncedFolder.isSubfolderByDate(), syncedFolder.getAccount(), syncedFolder.getUploadAction(), @@ -443,6 +444,7 @@ private SyncedFolderDisplayItem createSyncedFolder(@NonNull SyncedFolder syncedF syncedFolder.getRemotePath(), syncedFolder.isWifiOnly(), syncedFolder.isChargingOnly(), + syncedFolder.isExisting(), syncedFolder.isSubfolderByDate(), syncedFolder.getAccount(), syncedFolder.getUploadAction(), @@ -469,6 +471,7 @@ private SyncedFolderDisplayItem createSyncedFolderFromMediaFolder(@NonNull Media getString(R.string.instant_upload_path) + "/" + mediaFolder.folderName, true, false, + true, false, getAccount().name, FileUploader.LOCAL_BEHAVIOUR_FORGET, @@ -577,7 +580,7 @@ public boolean onOptionsItemSelected(MenuItem item) { 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 void onSyncStatusToggleClick(int section, SyncedFolderDisplayItem syncedF } if (syncedFolderDisplayItem.isEnabled()) { - FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolderDisplayItem); + FilesSyncHelper.insertAllDBEntriesForSyncedFolder(syncedFolderDisplayItem, true); showBatteryOptimizationInfo(); } @@ -709,18 +712,20 @@ public void onSaveSyncedFolderPreference(SyncedFolderParcelable syncedFolder) { 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 @@ private void saveOrUpdateSyncedFolder(SyncedFolderDisplayItem item) { // 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 @@ private void storeSyncedFolder(SyncedFolderDisplayItem item) { 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 void onDeleteSyncedFolderPreference(SyncedFolderParcelable syncedFolder) * @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 @@ private void updateSyncedFolderItem(SyncedFolderDisplayItem item, String remotePath, boolean wifiOnly, boolean chargingOnly, + boolean existing, boolean subfolderByDate, Integer uploadAction, boolean enabled) { @@ -806,6 +813,7 @@ private void updateSyncedFolderItem(SyncedFolderDisplayItem item, item.setRemotePath(remotePath); item.setWifiOnly(wifiOnly); item.setChargingOnly(chargingOnly); + item.setExisting(existing); item.setSubfolderByDate(subfolderByDate); item.setUploadAction(uploadAction); item.setEnabled(enabled, clock.getCurrentTime()); diff --git a/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java b/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java index 2ca1520000e1..492b89c9f9b2 100755 --- a/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java +++ b/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java @@ -45,6 +45,7 @@ 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 @@ private void setupContent() { 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 @@ private void refresh() { } // 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(); diff --git a/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java b/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java index a694f1a4c1a7..eef8dbed9fca 100755 --- a/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java +++ b/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java @@ -26,6 +26,7 @@ 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.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.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.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 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 void onBindFooterViewHolder(SectionedViewHolder holder, int section) { 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 void onBindViewHolder(SectionedViewHolder holder, int section, int relati }); } 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 void onBindViewHolder(SectionedViewHolder holder, int section, int relati // 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 void onBindViewHolder(SectionedViewHolder holder, int section, int relati } } + 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. diff --git a/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java b/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java index f60552ebfbbe..9ae8d452fb8a 100644 --- a/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java +++ b/src/main/java/com/owncloud/android/ui/asynctasks/CopyAndUploadContentUrisTask.java @@ -219,8 +219,7 @@ protected ResultCode doInBackground(Object[] params) { } 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 @@ private void requestUpload(Account account, String localPath, String remotePath, false, // do not create parent folder if not existent UploadFileOperation.CREATED_BY_USER, false, - false + false, + FileUploader.NameCollisionPolicy.ASK_USER ); } diff --git a/src/main/java/com/owncloud/android/ui/dialog/ConflictsResolveDialog.java b/src/main/java/com/owncloud/android/ui/dialog/ConflictsResolveDialog.java index 7c139e307af6..a231792fbada 100644 --- a/src/main/java/com/owncloud/android/ui/dialog/ConflictsResolveDialog.java +++ b/src/main/java/com/owncloud/android/ui/dialog/ConflictsResolveDialog.java @@ -43,44 +43,48 @@ public class ConflictsResolveDialog extends DialogFragment { public enum Decision { CANCEL, KEEP_BOTH, - OVERWRITE, - SERVER + KEEP_LOCAL, + KEEP_SERVER, } - OnConflictDecisionMadeListener mListener; + private final OnConflictDecisionMadeListener listener; + private final boolean canKeepServer; - public static ConflictsResolveDialog newInstance(OnConflictDecisionMadeListener listener) { - ConflictsResolveDialog f = new ConflictsResolveDialog(); - f.mListener = listener; - return f; + public ConflictsResolveDialog(OnConflictDecisionMadeListener listener, boolean canKeepServer) { + this.listener = listener; + this.canKeepServer = canKeepServer; } @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - return new AlertDialog.Builder(requireActivity(), R.style.Theme_ownCloud_Dialog) - .setIcon(R.drawable.ic_warning) - .setTitle(R.string.conflict_title) - .setMessage(getString(R.string.conflict_message)) - .setPositiveButton(R.string.conflict_use_local_version, - (dialog, which) -> { - if (mListener != null) { - mListener.conflictDecisionMade(Decision.OVERWRITE); - } - }) - .setNeutralButton(R.string.conflict_keep_both, - (dialog, which) -> { - if (mListener != null) { - mListener.conflictDecisionMade(Decision.KEEP_BOTH); - } - }) - .setNegativeButton(R.string.conflict_use_server_version, - (dialog, which) -> { - if (mListener != null) { - mListener.conflictDecisionMade(Decision.SERVER); - } - }) - .create(); + AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity(), R.style.Theme_ownCloud_Dialog) + .setIcon(R.drawable.ic_warning) + .setTitle(R.string.conflict_title) + .setMessage(getString(R.string.conflict_message)) + .setPositiveButton(R.string.conflict_use_local_version, + (dialog, which) -> { + if (listener != null) { + listener.conflictDecisionMade(Decision.KEEP_LOCAL); + } + }) + .setNeutralButton(R.string.conflict_keep_both, + (dialog, which) -> { + if (listener != null) { + listener.conflictDecisionMade(Decision.KEEP_BOTH); + } + }); + + if (this.canKeepServer) { + builder.setNegativeButton(R.string.conflict_use_server_version, + (dialog, which) -> { + if (listener != null) { + listener.conflictDecisionMade(Decision.KEEP_SERVER); + } + }); + } + + return builder.create(); } public void showDialog(AppCompatActivity activity) { @@ -96,8 +100,8 @@ public void showDialog(AppCompatActivity activity) { @Override public void onCancel(DialogInterface dialog) { - if (mListener != null) { - mListener.conflictDecisionMade(Decision.CANCEL); + if (listener != null) { + listener.conflictDecisionMade(Decision.CANCEL); } } diff --git a/src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.java b/src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.java index fbc07a310131..91ae78d593fe 100644 --- a/src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.java +++ b/src/main/java/com/owncloud/android/ui/dialog/SyncedFolderPreferencesDialogFragment.java @@ -77,6 +77,7 @@ public class SyncedFolderPreferencesDialogFragment extends DialogFragment { private SwitchCompat mEnabledSwitch; private AppCompatCheckBox mUploadOnWifiCheckbox; private AppCompatCheckBox mUploadOnChargingCheckbox; + private AppCompatCheckBox mUploadExistingCheckbox; private AppCompatCheckBox mUploadUseSubfoldersCheckbox; private TextView mUploadBehaviorSummary; private TextView mLocalFolderPath; @@ -189,6 +190,9 @@ private void setupDialogElements(View view) { 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 @@ private void setupDialogElements(View view) { 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 @@ private void setupViews(View view, boolean enable) { 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 void onClick(View v) { }); } + 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 diff --git a/src/main/java/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelable.java b/src/main/java/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelable.java index 2152eeb29e83..b59cf77625d3 100644 --- a/src/main/java/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelable.java +++ b/src/main/java/com/owncloud/android/ui/dialog/parcel/SyncedFolderParcelable.java @@ -41,6 +41,7 @@ public class SyncedFolderParcelable implements Parcelable { @Getter @Setter private String remotePath; @Getter @Setter private boolean wifiOnly = false; @Getter @Setter private boolean chargingOnly = false; + @Getter @Setter private boolean existing = true; @Getter @Setter private boolean enabled = false; @Getter @Setter private boolean subfolderByDate = false; @Getter private Integer uploadAction; @@ -57,6 +58,7 @@ public SyncedFolderParcelable(SyncedFolderDisplayItem syncedFolderDisplayItem, i 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 @@ private SyncedFolderParcelable(Parcel read) { 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 void writeToParcel(Parcel dest, int flags) { 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()); diff --git a/src/main/java/com/owncloud/android/ui/helpers/UriUploader.java b/src/main/java/com/owncloud/android/ui/helpers/UriUploader.java index e4c1432d178c..9dc46e3a2235 100644 --- a/src/main/java/com/owncloud/android/ui/helpers/UriUploader.java +++ b/src/main/java/com/owncloud/android/ui/helpers/UriUploader.java @@ -158,18 +158,18 @@ private String generateDiplayName() { * @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 ); } diff --git a/src/main/java/com/owncloud/android/utils/ErrorMessageAdapter.java b/src/main/java/com/owncloud/android/utils/ErrorMessageAdapter.java index 30a74bbb8656..94637e6087e4 100644 --- a/src/main/java/com/owncloud/android/utils/ErrorMessageAdapter.java +++ b/src/main/java/com/owncloud/android/utils/ErrorMessageAdapter.java @@ -76,14 +76,14 @@ public static String getErrorCauseMessage( 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 static String getErrorCauseMessage( * specific message for both. */ @Nullable - private static String getSpecificMessageForResultAndOperation( + private static String getMessageForResultAndOperation( RemoteOperationResult result, RemoteOperation operation, Resources res @@ -360,6 +360,9 @@ private static String getMessageForUploadFileOperation( } 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 @@ private static String getMessageForUploadFileOperation( * @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 @@ else if (!TextUtils.isEmpty(result.getHttpPhrase())) { * @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) { diff --git a/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java b/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java index e2074b313923..93ad2b35159a 100644 --- a/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java +++ b/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java @@ -79,13 +79,13 @@ private 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 static void insertAllDBEntriesForSyncedFolder(SyncedFolder syncedFolder) @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 FileVisitResult visitFileFailed(Path file, IOException exc) { 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 @@ private static void insertContentIntoDB(Uri uri, SyncedFolder syncedFolder) { 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 static void restartJobsIfNeeded(final UploadsStorageManager uploadsStorag 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 static void restartJobsIfNeeded(final UploadsStorageManager uploadsStorag 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(); } diff --git a/src/main/res/layout/synced_folders_settings_layout.xml b/src/main/res/layout/synced_folders_settings_layout.xml index ec3f63993f23..2c1d2af95511 100644 --- a/src/main/res/layout/synced_folders_settings_layout.xml +++ b/src/main/res/layout/synced_folders_settings_layout.xml @@ -19,12 +19,11 @@ License along with this program. If not, see . --> + 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:textAppearance="@style/TextAppearance.AppCompat.Title" /> - + android:textColor="?android:attr/textColorSecondary" /> + android:gravity="center" + android:padding="@dimen/standard_padding"> - + android:focusable="false" /> - + android:baselineAligned="false"> + android:padding="@dimen/standard_padding"> + android:textAppearance="?attr/textAppearanceListItem" /> - + android:textColor="?android:attr/textColorSecondary" /> + android:gravity="center" + android:padding="@dimen/standard_padding"> - + android:src="@drawable/ic_folder_open" /> - + android:baselineAligned="false"> + android:padding="@dimen/standard_padding"> + android:textAppearance="?attr/textAppearanceListItem" /> - + android:textColor="?android:attr/textColorSecondary" /> + android:gravity="center" + android:padding="@dimen/standard_padding"> - + android:src="@drawable/ic_folder_open" /> - + android:baselineAligned="false"> + android:padding="@dimen/standard_padding"> - + android:textAppearance="?attr/textAppearanceListItem" /> + android:gravity="center" + android:padding="@dimen/standard_padding"> - + android:focusable="false" /> - + android:baselineAligned="false"> + android:padding="@dimen/standard_padding"> - + android:textAppearance="?attr/textAppearanceListItem" /> + android:gravity="center" + android:padding="@dimen/standard_padding"> - + android:focusable="false" /> + + + + + + + + + + + + + android:baselineAligned="false"> + android:padding="@dimen/standard_padding"> + android:textAppearance="?attr/textAppearanceListItem" /> - + android:textColor="?android:attr/textColorSecondary" /> + android:gravity="center" + android:padding="@dimen/standard_padding"> - + android:focusable="false" /> - + android:padding="@dimen/standard_padding"> + android:textAppearance="?attr/textAppearanceListItem" /> - + android:textColor="?android:attr/textColorSecondary" /> - - + android:layout_height="wrap_content" + android:padding="@dimen/standard_padding"> + android:layout_alignParentLeft="true" + android:text="@string/common_delete" /> + android:layout_alignParentEnd="true" + android:layout_alignParentRight="true"> + android:text="@string/common_cancel" /> - + android:text="@string/common_save" /> - - diff --git a/src/main/res/menu/upload_list_item_file_conflict.xml b/src/main/res/menu/upload_list_item_file_conflict.xml new file mode 100644 index 000000000000..8a3a12ede1a4 --- /dev/null +++ b/src/main/res/menu/upload_list_item_file_conflict.xml @@ -0,0 +1,30 @@ + + + + + + + diff --git a/src/main/res/values/dims.xml b/src/main/res/values/dims.xml index 00860d18dc49..2176c6a3246f 100644 --- a/src/main/res/values/dims.xml +++ b/src/main/res/values/dims.xml @@ -135,6 +135,7 @@ 32dp 24dp -3dp + 80dp 12dp 16sp 18sp diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index a522c8b011fc..a8f3d431c62f 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -171,6 +171,7 @@ Unknown error Waiting for Wi-Fi Waiting to exit power save mode + Fetching server version… Waiting to upload %1$s (%2$d) Downloading… @@ -323,6 +324,7 @@ Only upload on unmetered Wi-Fi Only upload when charging + Also upload existing files /InstantUpload /AutoUpload File conflict @@ -912,6 +914,10 @@ Add folder info creates folder info edit folder info + File upload conflict + Pick which version to keep of %1$s + Resolve conflict + Delete Create new %1$s %2$s diff --git a/src/test/java/com/owncloud/android/ui/activity/SyncedFoldersActivityTest.java b/src/test/java/com/owncloud/android/ui/activity/SyncedFoldersActivityTest.java index 50046bb01bb3..29bffa5c3ce5 100644 --- a/src/test/java/com/owncloud/android/ui/activity/SyncedFoldersActivityTest.java +++ b/src/test/java/com/owncloud/android/ui/activity/SyncedFoldersActivityTest.java @@ -164,6 +164,7 @@ private SyncedFolderDisplayItem create(String folderName, boolean enabled) { true, true, true, + true, "test@nextcloud.com", 1, enabled,