Skip to content

Add forceSync option to auto-upload items #15267

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,22 @@

import android.content.Intent;
import android.os.Looper;
import android.view.Menu;
import android.view.MenuItem;

import com.nextcloud.client.preferences.SubFolderRule;
import com.owncloud.android.AbstractIT;
import com.owncloud.android.R;
import com.owncloud.android.databinding.SyncedFoldersLayoutBinding;
import com.owncloud.android.datamodel.MediaFolderType;
import com.owncloud.android.datamodel.SyncedFolder;
import com.owncloud.android.datamodel.SyncedFolderDisplayItem;
import com.owncloud.android.ui.activity.SyncedFoldersActivity;
import com.owncloud.android.ui.adapter.SyncedFolderAdapter;
import com.owncloud.android.ui.dialog.SyncedFolderPreferencesDialogFragment;
import com.owncloud.android.utils.ScreenshotTest;

import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;

Expand Down Expand Up @@ -49,24 +54,7 @@ public void open() {
@Test
@ScreenshotTest
public void testSyncedFolderDialog() {
SyncedFolderDisplayItem item = new SyncedFolderDisplayItem(1,
"/sdcard/DCIM/",
"/InstantUpload/",
true,
false,
false,
true,
"test@https://nextcloud.localhost",
0,
0,
true,
1000,
"Name",
MediaFolderType.IMAGE,
false,
SubFolderRule.YEAR_MONTH,
false,
SyncedFolder.NOT_SCANNED_YET);
SyncedFolderDisplayItem item = makeSyncedFolderDisplayItem(true);
SyncedFolderPreferencesDialogFragment sut = SyncedFolderPreferencesDialogFragment.newInstance(item, 0);

Intent intent = new Intent(targetContext, SyncedFoldersActivity.class);
Expand Down Expand Up @@ -99,4 +87,109 @@ public void showPowerCheckDialog() {

screenshot(Objects.requireNonNull(sut.getWindow()).getDecorView());
}

@Test
public void showForceSyncOption() {
SyncedFolderDisplayItem enabledItem = makeSyncedFolderDisplayItem(true);

Intent intent = new Intent(targetContext, SyncedFoldersActivity.class);
SyncedFoldersActivity activity = activityRule.launchActivity(intent);
activity.runOnUiThread(() -> {
activity.adapter.clear();
activity.adapter.addSyncFolderItem(enabledItem);
});

getInstrumentation().waitForIdleSync();

clickOnFolderItem(activity);

Menu menu = activity.adapter.getPopup$app_genericDebug().getMenu();
MenuItem forceView = menu.findItem(R.id.action_auto_upload_force_sync);
Assert.assertTrue(forceView.isEnabled());
Assert.assertTrue(forceView.isVisible());
}

@Test
public void notShowForceSyncOptionOnDisabledItem() {
SyncedFolderDisplayItem disabledItem = makeSyncedFolderDisplayItem(false);

Intent intent = new Intent(targetContext, SyncedFoldersActivity.class);
SyncedFoldersActivity activity = activityRule.launchActivity(intent);
activity.runOnUiThread(() -> {
activity.adapter.clear();
activity.adapter.addSyncFolderItem(disabledItem);
});

getInstrumentation().waitForIdleSync();

clickOnFolderItem(activity);

Menu menu = activity.adapter.getPopup$app_genericDebug().getMenu();
MenuItem forceView = menu.findItem(R.id.action_auto_upload_force_sync);
Assert.assertFalse(forceView.isEnabled());
Assert.assertFalse(forceView.isVisible());
}

@Test
@ScreenshotTest
public void showForceSyncDialog() {
SyncedFolderDisplayItem enabledItem = makeSyncedFolderDisplayItem(true);

Intent intent = new Intent(targetContext, SyncedFoldersActivity.class);
SyncedFoldersActivity activity = activityRule.launchActivity(intent);
activity.runOnUiThread(() -> {
activity.adapter.clear();
activity.adapter.addSyncFolderItem(enabledItem);
});

getInstrumentation().waitForIdleSync();

clickOnFolderItem(activity);

Menu menu = activity.adapter.getPopup$app_genericDebug().getMenu();
MenuItem forceView = menu.findItem(R.id.action_auto_upload_force_sync);

activity.runOnUiThread(() -> {
// I don't really see a nicer way to trigger this through the MenuItem
// there's no way to simulate a click on it and the interface does not expose its invoke function
activity.adapter.optionsItemSelected$app_genericDebug(forceView, 0, enabledItem);
});

getInstrumentation().waitForIdleSync();

screenshot(activity.getWindow().getDecorView());
}

private void clickOnFolderItem(SyncedFoldersActivity activity) {
activity.runOnUiThread(() -> {
SyncedFolderAdapter.HeaderViewHolder holder =
(SyncedFolderAdapter.HeaderViewHolder)activity.binding.list.findViewHolderForAdapterPosition(0);
holder
.getBinding()
.settingsButton
.performClick();
});
getInstrumentation().waitForIdleSync();
}

private SyncedFolderDisplayItem makeSyncedFolderDisplayItem(boolean enabled) {
return new SyncedFolderDisplayItem(1,
"/sdcard/DCIM/",
"/InstantUpload/",
true,
false,
false,
true,
"test@https://nextcloud.localhost",
0,
0,
enabled,
1000,
"Name",
MediaFolderType.IMAGE,
false,
SubFolderRule.YEAR_MONTH,
false,
SyncedFolder.NOT_SCANNED_YET);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ interface BackgroundJobManager {
fun startImmediateFilesSyncJob(
syncedFolderID: Long,
overridePowerSaving: Boolean = false,
changedFiles: Array<String?> = arrayOf<String?>()
changedFiles: Array<String?> = arrayOf<String?>(),
forceSync: Boolean = false
)

fun cancelTwoWaySyncJob()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -488,12 +488,14 @@ internal class BackgroundJobManagerImpl(
override fun startImmediateFilesSyncJob(
syncedFolderID: Long,
overridePowerSaving: Boolean,
changedFiles: Array<String?>
changedFiles: Array<String?>,
forceSync: Boolean
) {
val arguments = Data.Builder()
.putBoolean(FilesSyncWork.OVERRIDE_POWER_SAVING, overridePowerSaving)
.putStringArray(FilesSyncWork.CHANGED_FILES, changedFiles)
.putLong(FilesSyncWork.SYNCED_FOLDER_ID, syncedFolderID)
.putBoolean(FilesSyncWork.FORCE_SYNC, forceSync)
.build()

val request = oneTimeRequestBuilder(
Expand All @@ -505,7 +507,7 @@ internal class BackgroundJobManagerImpl(

workManager.enqueueUniqueWork(
JOB_IMMEDIATE_FILES_SYNC + "_" + syncedFolderID,
ExistingWorkPolicy.APPEND,
ExistingWorkPolicy.APPEND_OR_REPLACE,
request
)
}
Expand Down
34 changes: 30 additions & 4 deletions app/src/main/java/com/nextcloud/client/jobs/FilesSyncWork.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import android.text.TextUtils
import androidx.exifinterface.media.ExifInterface
import androidx.work.Worker
import androidx.work.WorkerParameters
import com.nextcloud.client.account.User
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.device.PowerManagementService
import com.nextcloud.client.jobs.upload.FileUploadHelper
Expand All @@ -29,6 +30,7 @@ import com.owncloud.android.datamodel.MediaFolderType
import com.owncloud.android.datamodel.SyncedFolder
import com.owncloud.android.datamodel.SyncedFolderProvider
import com.owncloud.android.datamodel.UploadsStorageManager
import com.owncloud.android.db.ProviderMeta.ProviderTableMeta
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.operations.UploadFileOperation
import com.owncloud.android.ui.activity.SettingsActivity
Expand Down Expand Up @@ -60,6 +62,7 @@ class FilesSyncWork(
const val OVERRIDE_POWER_SAVING = "overridePowerSaving"
const val CHANGED_FILES = "changedFiles"
const val SYNCED_FOLDER_ID = "syncedFolderId"
const val FORCE_SYNC = "forceSync"
}

private lateinit var syncedFolder: SyncedFolder
Expand All @@ -68,9 +71,11 @@ class FilesSyncWork(
override fun doWork(): Result {
val syncFolderId = inputData.getLong(SYNCED_FOLDER_ID, -1)
val changedFiles = inputData.getStringArray(CHANGED_FILES)
val forceSync = inputData.getBoolean(FORCE_SYNC, false)

backgroundJobManager.logStartOfWorker(BackgroundJobManagerImpl.formatClassTag(this::class) + "_" + syncFolderId)
Log_OC.d(TAG, "AutoUpload started folder ID: $syncFolderId")
Log_OC.d(TAG, "forceSync is: $forceSync")

// Create all the providers we'll need
val resources = context.resources
Expand All @@ -93,7 +98,8 @@ class FilesSyncWork(
filesystemDataProvider,
currentLocale,
dateFormat,
syncedFolder
syncedFolder,
forceSync
)

if (canExitEarly(changedFiles, syncFolderId)) {
Expand Down Expand Up @@ -123,7 +129,8 @@ class FilesSyncWork(
filesystemDataProvider,
currentLocale,
dateFormat,
syncedFolder
syncedFolder,
forceSync
)

FilesSyncHelper.restartUploadsIfNeeded(
Expand Down Expand Up @@ -223,6 +230,22 @@ class FilesSyncWork(
syncedFolderProvider.updateSyncFolder(syncedFolder)
}

/**
* [UploadFileOperation.grantFolderExistence] will later check whether the remote path for the given files already
* exists in the local [ProviderTableMeta.CONTENT_URI_DIR] table. If it does, we will not attempt to create the
* remote directory even if it does not exist anymore. This is fine if we assume that our local state always
* matches the remote state, but the two states are never reconciled. That's why during force sync,
* we want to attempt creating the remote dir even if it had already been created previously.
*/
private fun deleteContentDir(pathToDelete: String, user: User) {
val where = "path = ? AND file_owner = ?"
val params = arrayOf(pathToDelete, user.accountName)

Log_OC.d(TAG, "Deleting ${ProviderTableMeta.CONTENT_URI_DIR} entries for $pathToDelete and its children")
contentResolver.delete(ProviderTableMeta.CONTENT_URI_DIR, where, params)
Log_OC.d(TAG, "Done deleting entries for $pathToDelete")
}

@Suppress("LongMethod") // legacy code
private fun uploadFilesFromFolder(
context: Context,
Expand All @@ -231,7 +254,8 @@ class FilesSyncWork(
filesystemDataProvider: FilesystemDataProvider,
currentLocale: Locale,
sFormatter: SimpleDateFormat,
syncedFolder: SyncedFolder
syncedFolder: SyncedFolder,
forceSync: Boolean
) {
val uploadAction: Int?
val needsCharging: Boolean
Expand All @@ -250,14 +274,16 @@ class FilesSyncWork(
} else {
null
}
deleteContentDir(syncedFolder.remotePath, user)

// Ensure only new files are processed for upload.
// Files that have been previously uploaded cannot be re-uploaded,
// even if they have been deleted or moved from the target folder,
// as they are already marked as uploaded in the database.
val paths = filesystemDataProvider.getFilesForUpload(
syncedFolder.localPath,
syncedFolder.id.toString()
syncedFolder.id.toString(),
forceSync
)
if (paths.isEmpty()) {
Log_OC.w(TAG, "AutoUpload:uploadFilesFromFolder skipped paths is empty")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,19 +117,30 @@ public void storeOrUpdateFileValue(String localPath, long modifiedAt, boolean is
}
}

public Set<String> getFilesForUpload(String localPath, String syncedFolderId) {
public Set<String> getFilesForUpload(String localPath, String syncedFolderId, Boolean forceSync) {
Set<String> localPathsToUpload = new HashSet<>();

String likeParam = localPath + "%";

String selection = ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH + " LIKE ? and " +
ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID + " = ? and " +
ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_IS_FOLDER + " = ?";
String[] selectionArgs;

if (!forceSync) {
selection += " and " + ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_SENT_FOR_UPLOAD + " = ? ";
selectionArgs = new String[]{likeParam, syncedFolderId, "0", "0"};
} else {
// if forceSync, we want to also consider files that are already marked as FILESYSTEM_FILE_SENT_FOR_UPLOAD=1
// as sometimes files can get marked as uploaded even if they are not
selectionArgs = new String[]{likeParam, syncedFolderId, "0"};
}

Cursor cursor = contentResolver.query(
ProviderMeta.ProviderTableMeta.CONTENT_URI_FILESYSTEM,
null,
ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_LOCAL_PATH + " LIKE ? and " +
ProviderMeta.ProviderTableMeta.FILESYSTEM_SYNCED_FOLDER_ID + " = ? and " +
ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_SENT_FOR_UPLOAD + " = ? and " +
ProviderMeta.ProviderTableMeta.FILESYSTEM_FILE_IS_FOLDER + " = ?",
new String[]{likeParam, syncedFolderId, "0", "0"},
selection,
selectionArgs,
null);

if (cursor != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ package com.owncloud.android.ui.activity

import android.annotation.SuppressLint
import android.app.NotificationManager
import android.content.DialogInterface
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Bundle
Expand Down Expand Up @@ -612,6 +613,26 @@ class SyncedFoldersActivity :
checkAndShowEmptyListContent()
}

override fun onForceSyncClicked(section: Int, syncedFolder: SyncedFolderDisplayItem?) {
if (syncedFolder == null) return

MaterialAlertDialogBuilder(this, R.style.Theme_ownCloud_Dialog)
.setTitle(R.string.autoupload_force_sync)
.setMessage(R.string.autoupload_force_sync_desc)
.setPositiveButton(R.string.common_ok) { dialog: DialogInterface?, _: Int ->
Log_OC.d(TAG, "Starting forced sync for item ${syncedFolder.localPath}")
backgroundJobManager.startImmediateFilesSyncJob(syncedFolder.id, overridePowerSaving = true, forceSync = true)
dialog?.dismiss()
}
.setNegativeButton(R.string.common_cancel) { dialog: DialogInterface?, _: Int ->
dialog?.dismiss()
}
.setIcon(R.drawable.ic_warning)
.setIconAttribute(android.R.attr.alertDialogIcon)
.create()
.show()
}

private fun showEmptyContent(headline: String, message: String, action: String) {
showEmptyContent(headline, message)
binding.emptyList.emptyListViewAction.text = action
Expand Down
Loading