From 1b7bd72c494141fe51b25e6fb2e99ba76aadccc9 Mon Sep 17 00:00:00 2001 From: Chris Koying Browet Date: Fri, 10 Aug 2018 13:51:19 +0200 Subject: [PATCH 01/11] ADD: [instantupload] setting to also upload existing files Signed-off-by: Chris Koying Browet --- .../java/com/owncloud/android/MainApp.java | 2 +- .../android/datamodel/SyncedFolder.java | 9 +++- .../datamodel/SyncedFolderDisplayItem.java | 11 ++-- .../datamodel/SyncedFolderProvider.java | 8 ++- .../com/owncloud/android/db/ProviderMeta.java | 3 +- .../owncloud/android/jobs/FilesSyncJob.java | 2 +- .../providers/FileContentProvider.java | 19 +++++++ .../ui/activity/SyncedFoldersActivity.java | 28 +++++++---- ...SyncedFolderPreferencesDialogFragment.java | 17 +++++++ .../dialog/parcel/SyncedFolderParcelable.java | 4 ++ .../android/utils/FilesSyncHelper.java | 23 ++++++--- .../layout/synced_folders_settings_layout.xml | 50 +++++++++++++++++++ src/main/res/values/strings.xml | 1 + .../activity/SyncedFoldersActivityTest.java | 1 + 14 files changed, 150 insertions(+), 28 deletions(-) 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/db/ProviderMeta.java b/src/main/java/com/owncloud/android/db/ProviderMeta.java index b5e9d3e8b636..a8e88429ef9e 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 @@ -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/jobs/FilesSyncJob.java b/src/main/java/com/owncloud/android/jobs/FilesSyncJob.java index 0268bec03813..1955d3c009e2 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(); diff --git a/src/main/java/com/owncloud/android/providers/FileContentProvider.java b/src/main/java/com/owncloud/android/providers/FileContentProvider.java index 583d7576200c..9113c717447a 100644 --- a/src/main/java/com/owncloud/android/providers/FileContentProvider.java +++ b/src/main/java/com/owncloud/android/providers/FileContentProvider.java @@ -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,24 @@ 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"); + db.beginTransaction(); + try { + db.execSQL(ALTER_TABLE + ProviderTableMeta.SYNCED_FOLDERS_TABLE_NAME + + ADD_COLUMN + ProviderTableMeta.SYNCED_FOLDER_EXISTING + " INTEGER "); // boolean + + 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/SyncedFoldersActivity.java b/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.java index 6eb7b53f5ac6..3572f343a360 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(), @@ -470,6 +472,7 @@ private SyncedFolderDisplayItem createSyncedFolderFromMediaFolder(@NonNull Media true, false, false, + false, getAccount().name, FileUploader.LOCAL_BEHAVIOUR_FORGET, false, @@ -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, false, 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/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/utils/FilesSyncHelper.java b/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java index e2074b313923..432a9220d166 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); diff --git a/src/main/res/layout/synced_folders_settings_layout.xml b/src/main/res/layout/synced_folders_settings_layout.xml index ec3f63993f23..bc6b1f90721e 100644 --- a/src/main/res/layout/synced_folders_settings_layout.xml +++ b/src/main/res/layout/synced_folders_settings_layout.xml @@ -317,6 +317,56 @@ + + + + + + + + + + + + + + + + Only upload on unmetered Wi-Fi Only upload when charging + Also upload existing files /InstantUpload /AutoUpload File conflict 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, From 213002f44e45aac066dbdb47645cffb629070094 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Wed, 20 Nov 2019 13:29:21 +0100 Subject: [PATCH 02/11] Make newly created synced folders auto upload existing files by default Signed-off-by: Alice Gaudon --- .../owncloud/android/ui/activity/SyncedFoldersActivity.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 3572f343a360..62e3936fe7d6 100644 --- a/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.java +++ b/src/main/java/com/owncloud/android/ui/activity/SyncedFoldersActivity.java @@ -471,7 +471,7 @@ private SyncedFolderDisplayItem createSyncedFolderFromMediaFolder(@NonNull Media getString(R.string.instant_upload_path) + "/" + mediaFolder.folderName, true, false, - false, + true, false, getAccount().name, FileUploader.LOCAL_BEHAVIOUR_FORGET, @@ -580,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, 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); From 6783e8ab4c9f3229107aa0bd0e359c2aff56844e Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Mon, 25 Nov 2019 14:36:58 +0100 Subject: [PATCH 03/11] Make file uploads ask the user what to do when the file already exists on remote Signed-off-by: Alice Gaudon --- .../java/com/owncloud/android/UploadIT.java | 24 ++-- .../datamodel/UploadsStorageManager.java | 6 +- .../com/owncloud/android/db/OCUpload.java | 10 +- .../com/owncloud/android/db/ProviderMeta.java | 2 +- .../files/services/FileDownloader.java | 11 ++ .../android/files/services/FileUploader.java | 61 +++++++--- .../owncloud/android/jobs/FilesSyncJob.java | 24 ++-- .../operations/SynchronizeFileOperation.java | 3 +- .../operations/UploadFileOperation.java | 101 ++++++++------- .../providers/FileContentProvider.java | 51 +++++++- .../ui/activity/ConflictsResolveActivity.java | 107 ++++++++++------ .../ui/activity/UploadListActivity.java | 8 +- .../android/ui/adapter/UploadListAdapter.java | 115 +++++++++++++++--- .../ui/dialog/ConflictsResolveDialog.java | 68 ++++++----- src/main/res/values/strings.xml | 1 + 15 files changed, 401 insertions(+), 191 deletions(-) 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/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 a8e88429ef9e..5953bab1c3b1 100644 --- a/src/main/java/com/owncloud/android/db/ProviderMeta.java +++ b/src/main/java/com/owncloud/android/db/ProviderMeta.java @@ -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"; 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..0540acab4c62 100644 --- a/src/main/java/com/owncloud/android/files/services/FileUploader.java +++ b/src/main/java/com/owncloud/android/files/services/FileUploader.java @@ -148,9 +148,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. */ @@ -256,7 +257,7 @@ public void uploadNewFile( } } - public void uploadFileWithOverwrite( + public void uploadFileWithNameCollisionPolicy( Context context, Account account, String[] localPaths, @@ -267,7 +268,7 @@ public void uploadFileWithOverwrite( int createdBy, boolean requiresWifi, boolean requiresCharging, - boolean overwrite + NameCollisionPolicy nameCollisionPolicy ) { Intent intent = new Intent(context, FileUploader.class); @@ -280,7 +281,7 @@ public void uploadFileWithOverwrite( 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); + intent.putExtra(FileUploader.KEY_NAME_COLLISION_POLICY, nameCollisionPolicy); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { context.startForegroundService(intent); @@ -292,11 +293,11 @@ public void uploadFileWithOverwrite( /** * Call to upload a file */ - public void uploadFileWithOverwrite(Context context, Account account, String localPath, String remotePath, int + public void uploadFileWithNameCollisionPolicy(Context context, Account account, String localPath, String remotePath, int behaviour, String mimeType, boolean createRemoteFile, int createdBy, boolean requiresWifi, - boolean requiresCharging, boolean overwrite) { + boolean requiresCharging, NameCollisionPolicy nameCollisionPolicy) { - uploadFileWithOverwrite( + uploadFileWithNameCollisionPolicy( context, account, new String[]{localPath}, @@ -307,7 +308,7 @@ public void uploadFileWithOverwrite(Context context, Account account, String loc createdBy, requiresWifi, requiresCharging, - overwrite + nameCollisionPolicy ); } @@ -336,13 +337,13 @@ public void uploadNewFile(Context context, Account account, String localPath, St * Call to update multiple files already uploaded */ public void uploadUpdate(Context context, Account account, OCFile[] existingFiles, Integer behaviour, - Boolean forceOverwrite) { + 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_FORCE_OVERWRITE, forceOverwrite); + intent.putExtra(FileUploader.KEY_NAME_COLLISION_POLICY, nameCollisionPolicy); if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { context.startForegroundService(intent); @@ -355,9 +356,9 @@ public void uploadUpdate(Context context, Account account, OCFile[] existingFile * Call to update a dingle file already uploaded */ public void uploadUpdate(Context context, Account account, OCFile existingFile, Integer behaviour, - Boolean forceOverwrite) { + NameCollisionPolicy nameCollisionPolicy) { - uploadUpdate(context, account, new OCFile[]{existingFile}, behaviour, forceOverwrite); + uploadUpdate(context, account, new OCFile[]{existingFile}, behaviour, nameCollisionPolicy); } @@ -617,7 +618,10 @@ public int onStartCommand(Intent intent, int flags, int startId) { } // at this point variable "OCFile[] files" is loaded correctly. - boolean forceOverwrite = intent.getBooleanExtra(KEY_FORCE_OVERWRITE, false); + 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); @@ -628,7 +632,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { OCUpload ocUpload = new OCUpload(file, account); ocUpload.setFileSize(file.getFileLength()); - ocUpload.setForceOverwrite(forceOverwrite); + ocUpload.setNameCollisionPolicy(nameCollisionPolicy); ocUpload.setCreateRemoteFolder(isCreateRemoteFolder); ocUpload.setCreatedBy(createdBy); ocUpload.setLocalAction(localAction); @@ -644,7 +648,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { account, file, ocUpload, - forceOverwrite, + nameCollisionPolicy, localAction, this, onWifiOnly, @@ -705,7 +709,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { account, null, upload, - upload.isForceOverwrite(), // TODO should be read from DB? + upload.getNameCollisionPolicy(), // TODO should be read from DB? upload.getLocalAction(), // TODO should be read from DB? this, onWifiOnly, @@ -868,9 +872,8 @@ public void clearListeners() { * 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. + * 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 @@ -1362,4 +1365,22 @@ private void cancelUploadsForAccount(Account account) { mPendingUploads.remove(account.name); mUploadsStorageManager.removeUploads(account.name); } + + public enum NameCollisionPolicy { + CANCEL, + RENAME, + OVERWRITE, + 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(); + } + } } diff --git a/src/main/java/com/owncloud/android/jobs/FilesSyncJob.java b/src/main/java/com/owncloud/android/jobs/FilesSyncJob.java index 1955d3c009e2..527749607f19 100644 --- a/src/main/java/com/owncloud/android/jobs/FilesSyncJob.java +++ b/src/main/java/com/owncloud/android/jobs/FilesSyncJob.java @@ -203,24 +203,24 @@ private void syncFolder(Context context, Resources resources, boolean lightVersi remotePath = syncedFolder.getRemotePath(); } - requester.uploadFileWithOverwrite( - context, - user.toPlatformAccount(), - file.getAbsolutePath(), - FileStorageUtils.getInstantUploadFilePath( + requester.uploadFileWithNameCollisionPolicy( + 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..fc0a71ccfb6d 100644 --- a/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java +++ b/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java @@ -292,7 +292,8 @@ protected RemoteOperationResult run(OwnCloudClient client) { */ private void requestForUpload(OCFile file) { FileUploader.UploadRequester requester = new FileUploader.UploadRequester(); - requester.uploadUpdate(mContext, mAccount, file, FileUploader.LOCAL_BEHAVIOUR_MOVE, true); + requester.uploadUpdate(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 9113c717447a..6959a5c2ad5c 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 @@ -2106,13 +2106,58 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 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"); + 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" + ", " + + 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 { 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..5b1038423028 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,82 @@ 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) { + this.conflictUpload = savedInstanceState.getParcelable(EXTRA_CONFLICT_UPLOAD); + this.localBehaviour = savedInstanceState.getInt(EXTRA_LOCAL_BEHAVIOUR); + } else { + this.conflictUpload = getIntent().getParcelableExtra(EXTRA_CONFLICT_UPLOAD); + this.localBehaviour = getIntent().getIntExtra(EXTRA_LOCAL_BEHAVIOUR, this.localBehaviour); + } + + if (this.conflictUpload != null) { + this.localBehaviour = this.conflictUpload.getLocalAction(); + } } @Override public void conflictDecisionMade(Decision decision) { + if (decision == Decision.CANCEL) { + return; + } - Integer behaviour = null; - Boolean forceOverwrite = null; + OCFile file = getFile(); + FileUploader.UploadRequester uploadRequester = new FileUploader.UploadRequester(); - switch (decision) { - case CANCEL: - finish(); - return; - case OVERWRITE: - // use local version -> overwrite on server - forceOverwrite = true; - break; - case KEEP_BOTH: - behaviour = FileUploader.LOCAL_BEHAVIOUR_MOVE; - break; - case SERVER: - // use server version -> delete local, request download - Intent intent = new Intent(this, FileDownloader.class); - intent.putExtra(FileDownloader.EXTRA_ACCOUNT, getAccount()); - intent.putExtra(FileDownloader.EXTRA_FILE, getFile()); - startService(intent); - finish(); - return; - default: - Log_OC.e(TAG, "Unhandled conflict decision " + decision); - return; + // Upload + if (decision == Decision.KEEP_LOCAL || decision == Decision.KEEP_BOTH) { + FileUploader.NameCollisionPolicy collisionPolicy = FileUploader.NameCollisionPolicy.OVERWRITE; + if (decision == Decision.KEEP_BOTH) { + collisionPolicy = FileUploader.NameCollisionPolicy.RENAME; + } + + uploadRequester.uploadUpdate(this, getAccount(), file, localBehaviour, collisionPolicy); + + if (this.conflictUpload != null) { + uploadsStorageManager.removeUpload(this.conflictUpload); + } + } + + // Download + if (decision == Decision.KEEP_SERVER && !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 (this.conflictUpload != null) { + intent.putExtra(FileDownloader.EXTRA_CONFLICT_UPLOAD, this.conflictUpload); + } + startService(intent); } - FileUploader.UploadRequester requester = new FileUploader.UploadRequester(); - requester.uploadUpdate(this, getAccount(), getFile(), behaviour, forceOverwrite); finish(); } @@ -87,26 +119,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/UploadListActivity.java b/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java index 2ca1520000e1..7cf5933b4492 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); 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..a1ef327da1fb 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; @@ -44,10 +45,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 +60,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 +85,11 @@ public class UploadListAdapter extends SectionedRecyclerViewAdapter - 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) { + 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()) { + if (optionalUser.isPresent()) { + Account userAccount = optionalUser.get().toPlatformAccount(); + this.refreshFolder(itemViewHolder, userAccount, folder, (caller, result) -> { + itemViewHolder.status.setText(status); + if (result.isSuccess()) { + OCFile file = storageManager.getFileByPath(remotePath); + if (file != null) { + this.openConflictActivity(file, item); + } + } + }); + } + return; + } + + // Destination folder doesn't exist anymore } - }); - } + + if (ocFile != null) { + this.openConflictActivity(ocFile, item); + return; + } + + // Remote file doesn't exist anymore = there is no more conflict + } + + // not a credentials error + 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 + ); + } + }); } else { itemViewHolder.itemLayout.setOnClickListener(v -> onUploadItemClick(item)); @@ -466,6 +511,36 @@ public void onBindViewHolder(SectionedViewHolder holder, int section, int relati } } + 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/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/res/values/strings.xml b/src/main/res/values/strings.xml index db5cb0863658..9dd9fdafaada 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… From 7713a28a4f13c70834b2cbac5302d7f0f3692425 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Mon, 9 Dec 2019 12:09:24 +0100 Subject: [PATCH 04/11] FileUploader: require explicit NameCollisionPolicy and change default used value - Changes NameConflictPolicy for manual uploads from default RENAME to ASK_USER - Removal of helper class FileUploader.UploadRequester (methods become static and go to FileUploader) Signed-off-by: Alice Gaudon --- .../android/files/services/FileUploader.java | 463 ++++++++---------- .../android/jobs/ContactsBackupJob.java | 24 +- .../owncloud/android/jobs/FilesSyncJob.java | 18 +- .../operations/SynchronizeFileOperation.java | 10 +- .../ui/activity/ConflictsResolveActivity.java | 3 +- .../ui/activity/FileDisplayActivity.java | 6 +- .../ReceiveExternalFilesActivity.java | 10 +- .../ui/activity/UploadListActivity.java | 17 +- .../android/ui/adapter/UploadListAdapter.java | 22 +- .../CopyAndUploadContentUrisTask.java | 6 +- .../android/ui/helpers/UriUploader.java | 24 +- .../android/utils/FilesSyncHelper.java | 18 +- 12 files changed, 284 insertions(+), 337 deletions(-) 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 0540acab4c62..6f67dea872dd 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; @@ -124,10 +125,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. */ @@ -175,13 +172,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; @@ -189,7 +189,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; @@ -197,17 +198,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() { @@ -215,249 +205,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 uploadFileWithNameCollisionPolicy( - 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 (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 uploadFileWithNameCollisionPolicy(Context context, Account account, String localPath, String remotePath, int - behaviour, String mimeType, boolean createRemoteFile, int createdBy, boolean requiresWifi, - boolean requiresCharging, NameCollisionPolicy nameCollisionPolicy) { - - uploadFileWithNameCollisionPolicy( - context, - account, - new String[]{localPath}, - new String[]{remotePath}, - new String[]{mimeType}, - behaviour, - createRemoteFile, - createdBy, - requiresWifi, - requiresCharging, - nameCollisionPolicy - ); - } - - /** - * 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, - 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 (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, - NameCollisionPolicy nameCollisionPolicy) { - - uploadUpdate(context, account, new OCFile[]{existingFile}, behaviour, nameCollisionPolicy); - } - - - /** - * 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 */ @@ -1359,13 +1106,207 @@ 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.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 && 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; + } + + public enum NameCollisionPolicy { CANCEL, RENAME, 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 527749607f19..f507fa224470 100644 --- a/src/main/java/com/owncloud/android/jobs/FilesSyncJob.java +++ b/src/main/java/com/owncloud/android/jobs/FilesSyncJob.java @@ -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,7 +207,7 @@ private void syncFolder(Context context, Resources resources, boolean lightVersi remotePath = syncedFolder.getRemotePath(); } - requester.uploadFileWithNameCollisionPolicy( + FileUploader.uploadNewFile( context, user.toPlatformAccount(), file.getAbsolutePath(), diff --git a/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java b/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java index fc0a71ccfb6d..d2ed15ccdd45 100644 --- a/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java +++ b/src/main/java/com/owncloud/android/operations/SynchronizeFileOperation.java @@ -291,9 +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, - FileUploader.NameCollisionPolicy.ASK_USER); + FileUploader.uploadUpdateFile( + mContext, + mAccount, + file, + FileUploader.LOCAL_BEHAVIOUR_MOVE, + FileUploader.NameCollisionPolicy.ASK_USER + ); mTransferWasRequested = true; } 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 5b1038423028..0be1bb2c4eb3 100644 --- a/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.java +++ b/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.java @@ -82,7 +82,6 @@ public void conflictDecisionMade(Decision decision) { } OCFile file = getFile(); - FileUploader.UploadRequester uploadRequester = new FileUploader.UploadRequester(); // Upload if (decision == Decision.KEEP_LOCAL || decision == Decision.KEEP_BOTH) { @@ -91,7 +90,7 @@ public void conflictDecisionMade(Decision decision) { collisionPolicy = FileUploader.NameCollisionPolicy.RENAME; } - uploadRequester.uploadUpdate(this, getAccount(), file, localBehaviour, collisionPolicy); + FileUploader.uploadUpdateFile(this, getAccount(), file, localBehaviour, collisionPolicy); if (this.conflictUpload != null) { uploadsStorageManager.removeUpload(this.conflictUpload); 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/UploadListActivity.java b/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java index 7cf5933b4492..492b89c9f9b2 100755 --- a/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java +++ b/src/main/java/com/owncloud/android/ui/activity/UploadListActivity.java @@ -220,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 a1ef327da1fb..e42810dc6af2 100755 --- a/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java +++ b/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java @@ -142,16 +142,15 @@ public void onBindHeaderViewHolder(SectionedViewHolder holder, int section, bool uploadsStorageManager.clearSuccessfulUploads(); break; case FAILED: - new Thread(() -> new FileUploader.UploadRequester() - .retryFailedUploads( - parentActivity, - null, - uploadsStorageManager, - connectivityService, - accountManager, - powerManagementService, - null)) - .start(); + new Thread(() -> FileUploader.retryFailedUploads( + parentActivity, + null, + uploadsStorageManager, + connectivityService, + accountManager, + powerManagementService, + null + )).start(); break; default: @@ -394,8 +393,7 @@ public void onBindViewHolder(SectionedViewHolder holder, int section, int relati // not a credentials error File file = new File(item.getLocalPath()); if (file.exists()) { - FileUploader.UploadRequester requester = new FileUploader.UploadRequester(); - requester.retry(parentActivity, accountManager, item); + FileUploader.retryUpload(parentActivity, item.getAccount(accountManager), item); loadUploadItemsFromDb(); } else { DisplayUtils.showSnackMessage( 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/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/FilesSyncHelper.java b/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java index 432a9220d166..93ad2b35159a 100644 --- a/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java +++ b/src/main/java/com/owncloud/android/utils/FilesSyncHelper.java @@ -196,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(); @@ -221,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(); } From ec27f840a68de0b8e06a7b492a1348caa37fb245 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Mon, 9 Dec 2019 12:24:17 +0100 Subject: [PATCH 05/11] FileUploader: code cleanup Signed-off-by: Alice Gaudon --- .../android/files/services/FileUploader.java | 795 +++++++++--------- 1 file changed, 382 insertions(+), 413 deletions(-) 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 6f67dea872dd..2194220d5a8c 100644 --- a/src/main/java/com/owncloud/android/files/services/FileUploader.java +++ b/src/main/java/com/owncloud/android/files/services/FileUploader.java @@ -80,7 +80,7 @@ import java.io.File; import java.util.AbstractList; import java.util.HashMap; -import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Vector; @@ -96,15 +96,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(); @@ -135,8 +134,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"; /** @@ -214,29 +212,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(); } @@ -276,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) { @@ -308,9 +303,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { boolean whileChargingOnly = intent.getBooleanExtra(KEY_WHILE_CHARGING_ONLY, false); if (!retry) { - - if (!(intent.hasExtra(KEY_LOCAL_FILE) || - intent.hasExtra(KEY_FILE))) { + 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; } @@ -324,7 +317,6 @@ public int onStartCommand(Intent intent, int flags, int startId) { 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); @@ -334,7 +326,6 @@ public int onStartCommand(Intent intent, int flags, int startId) { 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"); @@ -352,13 +343,13 @@ public int onStartCommand(Intent intent, int flags, int startId) { 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 + 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]); + + " and localPaths[i]:" + localPaths[i]); return Service.START_NOT_STICKY; } } @@ -366,7 +357,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { // at this point variable "OCFile[] files" is loaded correctly. NameCollisionPolicy nameCollisionPolicy = (NameCollisionPolicy) intent.getSerializableExtra(KEY_NAME_COLLISION_POLICY); - if(nameCollisionPolicy == null) { + if (nameCollisionPolicy == null) { nameCollisionPolicy = NameCollisionPolicy.DEFAULT; } int localAction = intent.getIntExtra(KEY_LOCAL_BEHAVIOUR, LOCAL_BEHAVIOUR_FORGET); @@ -376,7 +367,6 @@ public int onStartCommand(Intent intent, int flags, int startId) { UploadFileOperation newUpload; try { for (OCFile file : files) { - OCUpload ocUpload = new OCUpload(file, account); ocUpload.setFileSize(file.getFileLength()); ocUpload.setNameCollisionPolicy(nameCollisionPolicy); @@ -387,19 +377,18 @@ public int onStartCommand(Intent intent, int flags, int startId) { ocUpload.setWhileChargingOnly(whileChargingOnly); ocUpload.setUploadStatus(UploadStatus.UPLOAD_IN_PROGRESS); - newUpload = new UploadFileOperation( mUploadsStorageManager, - connectivityService, - powerManagementService, - account, - file, - ocUpload, - nameCollisionPolicy, - localAction, - this, - onWifiOnly, - whileChargingOnly + connectivityService, + powerManagementService, + account, + file, + ocUpload, + nameCollisionPolicy, + localAction, + this, + onWifiOnly, + whileChargingOnly ); newUpload.setCreatedBy(createdBy); if (isCreateRemoteFolder) { @@ -411,9 +400,9 @@ public int onStartCommand(Intent intent, int flags, int startId) { newUpload.addRenameUploadListener(this); Pair putResult = mPendingUploads.putIfAbsent( - account.name, - file.getRemotePath(), - newUpload + account.name, + file.getRemotePath(), + newUpload ); if (putResult != null) { uploadKey = putResult.first; @@ -424,19 +413,15 @@ public int onStartCommand(Intent intent, int flags, int startId) { newUpload.setOCUploadId(id); } } - } 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; - } // *** TODO REWRITE: block inserted to request A retry; too many code copied, no control exception ***/ } else { @@ -450,17 +435,17 @@ public int onStartCommand(Intent intent, int flags, int startId) { whileChargingOnly = upload.isWhileChargingOnly(); UploadFileOperation newUpload = new UploadFileOperation( - mUploadsStorageManager, - connectivityService, - powerManagementService, - account, - null, - upload, - upload.getNameCollisionPolicy(), // TODO should be read from DB? - upload.getLocalAction(), // TODO should be read from DB? - this, - onWifiOnly, - whileChargingOnly + mUploadsStorageManager, + connectivityService, + powerManagementService, + account, + null, + upload, + upload.getNameCollisionPolicy(), // TODO should be read from DB? + upload.getLocalAction(), // TODO should be read from DB? + this, + onWifiOnly, + whileChargingOnly ); newUpload.addDataTransferProgressListener(this); @@ -469,10 +454,10 @@ public int onStartCommand(Intent intent, int flags, int startId) { newUpload.addRenameUploadListener(this); Pair putResult = mPendingUploads.putIfAbsent( - account.name, - upload.getRemotePath(), - newUpload - ); + account.name, + upload.getRemotePath(), + newUpload + ); if (putResult != null) { String uploadKey = putResult.first; requestedUploads.add(uploadKey); @@ -495,11 +480,10 @@ public int onStartCommand(Intent intent, int flags, int startId) { } /** - * 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) { @@ -517,304 +501,26 @@ 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(); } // The rest of uploads are cancelled when they try to start } - /** - * 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 - * - * 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); - } - - 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"); + " does not exist anymore -> cancelling all its uploads"); cancelUploadsForAccount(mCurrentUpload.getAccount()); return; } @@ -832,19 +538,12 @@ public void uploadFile(String uploadKey) { /// 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() - ); + 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); + OwnCloudAccount ocAccount = new OwnCloudAccount(mCurrentAccount, this); + mUploadClient = OwnCloudClientManagerFactory.getDefaultSingleton().getClientFor(ocAccount, this); // // If parent folder is encrypted, upload file encrypted @@ -856,27 +555,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); @@ -889,7 +585,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(); @@ -909,16 +605,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); } @@ -928,7 +624,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) { @@ -945,8 +641,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); @@ -964,8 +664,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) { @@ -981,7 +680,7 @@ 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; @@ -992,11 +691,11 @@ private void notifyUploadResult(UploadFileOperation upload, tickerId = needsToUpdateCredentials ? R.string.uploader_upload_failed_credentials_error : tickerId; 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()); @@ -1004,31 +703,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); @@ -1037,8 +736,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 */ @@ -1050,16 +748,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()); @@ -1070,8 +765,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 * @@ -1080,10 +774,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 @@ -1324,4 +1018,279 @@ 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); + } + } } From 52d089b18a47f040ecd078770852d5a689b57ef9 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Sun, 8 Dec 2019 00:11:28 +0100 Subject: [PATCH 06/11] FileUploader: fix codacy issues and SpotBugs Signed-off-by: Alice Gaudon --- .../android/files/services/FileUploader.java | 368 ++++++++++-------- 1 file changed, 210 insertions(+), 158 deletions(-) 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 2194220d5a8c..1e50f8580643 100644 --- a/src/main/java/com/owncloud/android/files/services/FileUploader.java +++ b/src/main/java/com/owncloud/android/files/services/FileUploader.java @@ -78,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.List; import java.util.Map; -import java.util.Vector; import javax.annotation.Nullable; import javax.inject.Inject; @@ -253,6 +252,7 @@ private void resurrection() { /** * Service clean up */ + @SuppressWarnings("PMD.NullAssignment") @Override public void onDestroy() { Log_OC.v(TAG, "Destroying service"); @@ -297,186 +297,238 @@ 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 (!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; - - 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); + 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; + } + + /** + * 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); + } - if (intent.hasExtra(KEY_FILE) && files == null) { - Log_OC.e(TAG, "Incorrect array for OCFiles provided in upload intent"); + 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; - } 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!"); - 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!"); + 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; - } + 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. + } + // at this point variable "OCFile[] files" is loaded correctly. - NameCollisionPolicy nameCollisionPolicy = (NameCollisionPolicy) intent.getSerializableExtra(KEY_NAME_COLLISION_POLICY); - if (nameCollisionPolicy == null) { - nameCollisionPolicy = NameCollisionPolicy.DEFAULT; + 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 + ); } - 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.setNameCollisionPolicy(nameCollisionPolicy); - 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, - nameCollisionPolicy, - localAction, - this, - onWifiOnly, - whileChargingOnly - ); - newUpload.setCreatedBy(createdBy); - if (isCreateRemoteFolder) { - newUpload.setRemoteFolderToBeCreated(); - } - newUpload.addDataTransferProgressListener(this); - newUpload.addDataTransferProgressListener((FileUploaderBinder) mBinder); + } 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; + } - newUpload.addRenameUploadListener(this); + /** + * 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); - Pair putResult = mPendingUploads.putIfAbsent( - account.name, - file.getRemotePath(), - newUpload - ); - if (putResult != null) { - uploadKey = putResult.first; - requestedUploads.add(uploadKey); + newUpload.addRenameUploadListener(this); - // Save upload in database - long id = mUploadsStorageManager.storeUpload(ocUpload); - newUpload.setOCUploadId(id); - } - } - } 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; - } - // *** 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); - - onWifiOnly = upload.isUseWifiOnly(); - whileChargingOnly = upload.isWhileChargingOnly(); - - UploadFileOperation newUpload = new UploadFileOperation( - mUploadsStorageManager, - connectivityService, - powerManagementService, - account, - null, - upload, - upload.getNameCollisionPolicy(), // TODO should be read from DB? - upload.getLocalAction(), // TODO should be read from DB? - this, - onWifiOnly, - whileChargingOnly - ); + Pair putResult = mPendingUploads.putIfAbsent( + account.name, + file.getRemotePath(), + newUpload + ); - newUpload.addDataTransferProgressListener(this); - newUpload.addDataTransferProgressListener((FileUploaderBinder) mBinder); + if (putResult != null) { + requestedUploads.add(putResult.first); - newUpload.addRenameUploadListener(this); + // Save upload in database + long id = mUploadsStorageManager.storeUpload(ocUpload); + newUpload.setOCUploadId(id); + } + } - Pair putResult = mPendingUploads.putIfAbsent( - account.name, - upload.getRemotePath(), - newUpload - ); - if (putResult != null) { - String uploadKey = putResult.first; - requestedUploads.add(uploadKey); + /** + * 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 + ); - // Update upload in database - upload.setUploadStatus(UploadStatus.UPLOAD_IN_PROGRESS); - mUploadsStorageManager.updateUpload(upload); - } - } - // *** TODO REWRITE END ***/ + newUpload.addDataTransferProgressListener(this); + newUpload.addDataTransferProgressListener((FileUploaderBinder) mBinder); - if (requestedUploads.size() > 0) { - Message msg = mServiceHandler.obtainMessage(); - msg.arg1 = startId; - msg.obj = requestedUploads; - mServiceHandler.sendMessage(msg); - sendBroadcastUploadsAdded(); + newUpload.addRenameUploadListener(this); + + Pair putResult = mPendingUploads.putIfAbsent( + account.name, + upload.getRemotePath(), + newUpload + ); + if (putResult != null) { + String uploadKey = putResult.first; + requestedUploads.add(uploadKey); + + // Update upload in database + upload.setUploadStatus(UploadStatus.UPLOAD_IN_PROGRESS); + mUploadsStorageManager.updateUpload(upload); } - return Service.START_NOT_STICKY; } /** @@ -959,14 +1011,14 @@ public static void retryFailedUploads( for (OCUpload failedUpload : failedUploads) { accountMatch = account == null || account.name.equals(failedUpload.getAccountName()); - resultMatch = uploadResult == null || uploadResult.equals(failedUpload.getLastResult()); + 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().equals(UploadResult.FILE_NOT_FOUND)) { + if (failedUpload.getLastResult() != UploadResult.FILE_NOT_FOUND) { failedUpload.setLastResult(UploadResult.FILE_NOT_FOUND); uploadsStorageManager.updateUpload(failedUpload); } From 91ef307e7a1a32e32a9528ec187d92efa2a2afb9 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Mon, 9 Dec 2019 13:11:45 +0100 Subject: [PATCH 07/11] Fix database migration of field forceOverwrite to NameCollisionPolicy Signed-off-by: Alice Gaudon --- .../com/owncloud/android/files/services/FileUploader.java | 7 +++++-- .../owncloud/android/providers/FileContentProvider.java | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) 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 1e50f8580643..ad61edd66230 100644 --- a/src/main/java/com/owncloud/android/files/services/FileUploader.java +++ b/src/main/java/com/owncloud/android/files/services/FileUploader.java @@ -1053,10 +1053,13 @@ public static String getUploadFinishMessage() { } + /** + * 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, - RENAME, - OVERWRITE, ASK_USER; public static final NameCollisionPolicy DEFAULT = RENAME; diff --git a/src/main/java/com/owncloud/android/providers/FileContentProvider.java b/src/main/java/com/owncloud/android/providers/FileContentProvider.java index 6959a5c2ad5c..3fd9a3af48da 100644 --- a/src/main/java/com/owncloud/android/providers/FileContentProvider.java +++ b/src/main/java/com/owncloud/android/providers/FileContentProvider.java @@ -2147,7 +2147,7 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { ProviderTableMeta.UPLOADS_STATUS + ", " + ProviderTableMeta.UPLOADS_LOCAL_BEHAVIOUR + ", " + ProviderTableMeta.UPLOADS_UPLOAD_TIME + ", " + - "force_overwrite" + ", " + + "force_overwrite" + ", " + // See FileUploader.NameCollisionPolicy ProviderTableMeta.UPLOADS_IS_CREATE_REMOTE_FOLDER + ", " + ProviderTableMeta.UPLOADS_UPLOAD_END_TIMESTAMP + ", " + ProviderTableMeta.UPLOADS_LAST_RESULT + ", " + From e77ffa00a8bb5c32259d4e705050311e90ff8511 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Tue, 10 Dec 2019 11:06:57 +0100 Subject: [PATCH 08/11] Fix synced folder layout form control widgets alignment Signed-off-by: Alice Gaudon --- .../layout/synced_folders_settings_layout.xml | 245 ++++++------------ src/main/res/values/dims.xml | 1 + 2 files changed, 83 insertions(+), 163 deletions(-) diff --git a/src/main/res/layout/synced_folders_settings_layout.xml b/src/main/res/layout/synced_folders_settings_layout.xml index bc6b1f90721e..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: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/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 From a4ff1f11d7d6e9973227736d7123fb27d05e5515 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Tue, 10 Dec 2019 12:22:32 +0100 Subject: [PATCH 09/11] Customize upload sync conflict notification strings Signed-off-by: Alice Gaudon --- .../android/files/services/FileUploader.java | 8 ++++++-- .../android/utils/ErrorMessageAdapter.java | 16 +++++++++------- src/main/res/values/strings.xml | 2 ++ 3 files changed, 17 insertions(+), 9 deletions(-) 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 ad61edd66230..c958537b578a 100644 --- a/src/main/java/com/owncloud/android/files/services/FileUploader.java +++ b/src/main/java/com/owncloud/android/files/services/FileUploader.java @@ -739,8 +739,12 @@ private void notifyUploadResult(UploadFileOperation upload, RemoteOperationResul 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)) 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/res/values/strings.xml b/src/main/res/values/strings.xml index 9dd9fdafaada..9976e397a2b5 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -914,6 +914,8 @@ Add folder info creates folder info edit folder info + File upload conflict + Pick which version to keep of %1$s Create new %1$s %2$s From 6540e3ef71f13f17552dba6d1e6f37696499a0b9 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Tue, 10 Dec 2019 12:24:46 +0100 Subject: [PATCH 10/11] Upload list conflicts: replace trash icon with menu and add resolve action Signed-off-by: Alice Gaudon --- .../android/ui/adapter/UploadListAdapter.java | 115 ++++++++++++------ .../menu/upload_list_item_file_conflict.xml | 30 +++++ src/main/res/values/strings.xml | 2 + 3 files changed, 107 insertions(+), 40 deletions(-) create mode 100644 src/main/res/menu/upload_list_item_file_conflict.xml 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 e42810dc6af2..eef8dbed9fca 100755 --- a/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java +++ b/src/main/java/com/owncloud/android/ui/adapter/UploadListAdapter.java @@ -38,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; @@ -335,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); } @@ -354,40 +363,13 @@ public void onBindViewHolder(SectionedViewHolder holder, int section, int relati final UploadResult uploadResult = item.getLastResult(); itemViewHolder.itemLayout.setOnClickListener(v -> { if (uploadResult == UploadResult.CREDENTIAL_ERROR) { - parentActivity.getFileOperationsHelper().checkCurrentCredentials( - item.getAccount(accountManager)); + parentActivity.getFileOperationsHelper().checkCurrentCredentials(item.getAccount(accountManager)); return; - } else if (uploadResult == UploadResult.SYNC_CONFLICT) { - 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()) { - if (optionalUser.isPresent()) { - Account userAccount = optionalUser.get().toPlatformAccount(); - this.refreshFolder(itemViewHolder, userAccount, folder, (caller, result) -> { - itemViewHolder.status.setText(status); - if (result.isSuccess()) { - OCFile file = storageManager.getFileByPath(remotePath); - if (file != null) { - this.openConflictActivity(file, item); - } - } - }); - } - return; - } - - // Destination folder doesn't exist anymore - } - - if (ocFile != null) { - this.openConflictActivity(ocFile, item); + } else if (uploadResult == UploadResult.SYNC_CONFLICT && optionalUser.isPresent()) { + Account account = optionalUser.get().toPlatformAccount(); + if (checkAndOpenConflictResolutionDialog(itemViewHolder, item, account, status)) { return; } - - // Remote file doesn't exist anymore = there is no more conflict } // not a credentials error @@ -403,8 +385,7 @@ public void onBindViewHolder(SectionedViewHolder holder, int section, int relati } }); } else { - itemViewHolder.itemLayout.setOnClickListener(v -> - onUploadItemClick(item)); + itemViewHolder.itemLayout.setOnClickListener(v -> onUploadItemClick(item)); } // Set icon or thumbnail @@ -509,6 +490,60 @@ 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); 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/strings.xml b/src/main/res/values/strings.xml index 9976e397a2b5..a8f3d431c62f 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -916,6 +916,8 @@ edit folder info File upload conflict Pick which version to keep of %1$s + Resolve conflict + Delete Create new %1$s %2$s From 49f3a67251eac53b8456efa1360e7948a3489118 Mon Sep 17 00:00:00 2001 From: Alice Gaudon Date: Wed, 22 Jan 2020 16:45:18 +0100 Subject: [PATCH 11/11] ConflictsResolveActivity: code style and readability improvements Signed-off-by: Alice Gaudon Signed-off-by: tobiasKaminsky --- scripts/analysis/findbugs-results.txt | 2 +- .../ui/activity/ConflictsResolveActivity.java | 75 +++++++++++-------- 2 files changed, 46 insertions(+), 31 deletions(-) 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/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.java b/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.java index 0be1bb2c4eb3..9180a86f4348 100644 --- a/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.java +++ b/src/main/java/com/owncloud/android/ui/activity/ConflictsResolveActivity.java @@ -63,15 +63,15 @@ protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState != null) { - this.conflictUpload = savedInstanceState.getParcelable(EXTRA_CONFLICT_UPLOAD); - this.localBehaviour = savedInstanceState.getInt(EXTRA_LOCAL_BEHAVIOUR); + conflictUpload = savedInstanceState.getParcelable(EXTRA_CONFLICT_UPLOAD); + localBehaviour = savedInstanceState.getInt(EXTRA_LOCAL_BEHAVIOUR); } else { - this.conflictUpload = getIntent().getParcelableExtra(EXTRA_CONFLICT_UPLOAD); - this.localBehaviour = getIntent().getIntExtra(EXTRA_LOCAL_BEHAVIOUR, this.localBehaviour); + conflictUpload = getIntent().getParcelableExtra(EXTRA_CONFLICT_UPLOAD); + localBehaviour = getIntent().getIntExtra(EXTRA_LOCAL_BEHAVIOUR, localBehaviour); } - if (this.conflictUpload != null) { - this.localBehaviour = this.conflictUpload.getLocalAction(); + if (conflictUpload != null) { + localBehaviour = conflictUpload.getLocalAction(); } } @@ -83,30 +83,45 @@ public void conflictDecisionMade(Decision decision) { OCFile file = getFile(); - // Upload - if (decision == Decision.KEEP_LOCAL || decision == Decision.KEEP_BOTH) { - FileUploader.NameCollisionPolicy collisionPolicy = FileUploader.NameCollisionPolicy.OVERWRITE; - if (decision == Decision.KEEP_BOTH) { - collisionPolicy = FileUploader.NameCollisionPolicy.RENAME; - } - - FileUploader.uploadUpdateFile(this, getAccount(), file, localBehaviour, collisionPolicy); - - if (this.conflictUpload != null) { - uploadsStorageManager.removeUpload(this.conflictUpload); - } - } - - // Download - if (decision == Decision.KEEP_SERVER && !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 (this.conflictUpload != null) { - intent.putExtra(FileDownloader.EXTRA_CONFLICT_UPLOAD, this.conflictUpload); - } - startService(intent); + switch (decision) { + case KEEP_LOCAL: // Upload + FileUploader.uploadUpdateFile( + this, + getAccount(), + file, + localBehaviour, + FileUploader.NameCollisionPolicy.OVERWRITE + ); + + if (conflictUpload != null) { + uploadsStorageManager.removeUpload(conflictUpload); + } + break; + case KEEP_BOTH: // Upload + FileUploader.uploadUpdateFile( + this, + getAccount(), + file, + localBehaviour, + FileUploader.NameCollisionPolicy.RENAME + ); + + if (conflictUpload != null) { + uploadsStorageManager.removeUpload(conflictUpload); + } + break; + case KEEP_SERVER: // Download + if (!this.shouldDeleteLocal()) { + // Overwrite local file + Intent intent = new Intent(this, FileDownloader.class); + intent.putExtra(FileDownloader.EXTRA_ACCOUNT, getAccount()); + intent.putExtra(FileDownloader.EXTRA_FILE, file); + if (conflictUpload != null) { + intent.putExtra(FileDownloader.EXTRA_CONFLICT_UPLOAD, conflictUpload); + } + startService(intent); + } + break; } finish();