diff --git a/app/src/main/java/foundation/e/drive/contentScanner/AbstractContentScanner.java b/app/src/main/java/foundation/e/drive/contentScanner/AbstractContentScanner.java index cd8416189e74f94884a366fe3020e9dcdddd3ec8..4917c25ef255e687df495a6ffcbdad0a0009eec4 100644 --- a/app/src/main/java/foundation/e/drive/contentScanner/AbstractContentScanner.java +++ b/app/src/main/java/foundation/e/drive/contentScanner/AbstractContentScanner.java @@ -7,7 +7,6 @@ */ package foundation.e.drive.contentScanner; -import android.accounts.Account; import android.content.Context; import com.owncloud.android.lib.resources.files.FileUtils; @@ -27,18 +26,16 @@ import foundation.e.drive.models.SyncedFolder; */ public abstract class AbstractContentScanner { protected final Context context; - protected final Account account; protected final HashMap syncRequests; protected final List syncedFolders; /** * @param context Context used to access Database, etc. - * @param account Account used to checked if user has change some synchronization's settings + * @param syncedFolders List of SyncedFolders */ - protected AbstractContentScanner(Context context, Account account, List syncedFolders) { + protected AbstractContentScanner(Context context, List syncedFolders) { syncRequests = new HashMap<>(); this.context = context; - this.account = account; this.syncedFolders = syncedFolders; } @@ -66,8 +63,8 @@ public abstract class AbstractContentScanner { onNewFileFound(file); } - for(SyncedFileState remainingFileState : fileStates) { - onMissingRemoteFile(remainingFileState); + for (SyncedFileState remainingFileState : fileStates) { + onMissingFile(remainingFileState); } return syncRequests; }; @@ -81,7 +78,7 @@ public abstract class AbstractContentScanner { protected SyncedFolder getParentSyncedFolder(String filePath) { final String dirPath = filePath.substring(0, filePath.lastIndexOf(FileUtils.PATH_SEPARATOR) + 1); - for(SyncedFolder syncedFolder : syncedFolders) { + for (SyncedFolder syncedFolder : syncedFolders) { if (isSyncedFolderParentOfFile(syncedFolder, dirPath)) { return syncedFolder; } @@ -93,7 +90,7 @@ public abstract class AbstractContentScanner { * When a file doesn't exist anymore we remove it from device/cloud (depending of implementation) & from Database * @param fileState SyncedFileState for which we lack remote file */ - protected abstract void onMissingRemoteFile(SyncedFileState fileState); + protected abstract void onMissingFile(SyncedFileState fileState); /** * A new file has been found diff --git a/app/src/main/java/foundation/e/drive/contentScanner/LocalContentScanner.java b/app/src/main/java/foundation/e/drive/contentScanner/LocalContentScanner.java index bf988fe0fd050625ffb03313cbde6d06e06248d2..0187ac57bfa6906e944995fe43ccd5beefc0d5da 100644 --- a/app/src/main/java/foundation/e/drive/contentScanner/LocalContentScanner.java +++ b/app/src/main/java/foundation/e/drive/contentScanner/LocalContentScanner.java @@ -1,5 +1,5 @@ /* - * Copyright © ECORP SAS 2022. + * Copyright © MURENA SAS 2022. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0 * which accompanies this distribution, and is available at @@ -7,7 +7,6 @@ */ package foundation.e.drive.contentScanner; -import android.accounts.Account; import android.content.Context; import java.io.File; @@ -24,16 +23,17 @@ import timber.log.Timber; /** * Class to encapsulate function about scanning local file and * create syncRequest when needed + * @author vincent Bourgmayer */ public class LocalContentScanner extends AbstractContentScanner{ - public LocalContentScanner(Context context, Account account, List syncedFolders) { - super(context, account, syncedFolders); + public LocalContentScanner(Context context, List syncedFolders) { + super(context, syncedFolders); Timber.tag(LocalContentScanner.class.getSimpleName()); } @Override - protected void onMissingRemoteFile(SyncedFileState fileState) { + protected void onMissingFile(SyncedFileState fileState) { if (!fileState.hasBeenSynchronizedOnce()) { return; } @@ -61,12 +61,10 @@ public class LocalContentScanner extends AbstractContentScanner{ if (parentDir.isScanLocal()) scannableValue += 2; } - //create the syncedFile State final SyncedFileState newSyncedFileState = new SyncedFileState(-1, file.getName(), filePath, parentDir.getRemoteFolder() + file.getName(), "", 0, parentDir.getId(), parentDir.isMediaType(),scannableValue); - //Store it in DB - int storedId = DbHelper.manageSyncedFileStateDB(newSyncedFileState, "INSERT", context); - if (storedId > 0){ + final int storedId = DbHelper.manageSyncedFileStateDB(newSyncedFileState, "INSERT", context); + if (storedId > 0) { newSyncedFileState.setId( storedId ); Timber.d("Add upload SyncRequest for new file %s", filePath); syncRequests.put(storedId, new SyncRequest(newSyncedFileState, SyncRequest.Type.UPLOAD)); diff --git a/app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java b/app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java index 5f32c9acf665822d3b6171c7b0cc23395be066c7..342d9505fe61c56ebf77a19e60c44afafcaed721 100644 --- a/app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java +++ b/app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java @@ -1,5 +1,5 @@ /* - * Copyright © ECORP SAS 2022. + * Copyright © MURENA SAS 2022. * All rights reserved. This program and the accompanying materials * are made available under the terms of the GNU Public License v3.0 * which accompanies this distribution, and is available at @@ -7,19 +7,18 @@ */ package foundation.e.drive.contentScanner; +import static foundation.e.drive.models.SyncRequest.Type.LOCAL_DELETE; import static foundation.e.drive.utils.FileDiffUtils.getActionForFileDiff; -import android.accounts.Account; import android.content.Context; -import android.provider.MediaStore; import com.owncloud.android.lib.resources.files.model.RemoteFile; -import java.io.File; import java.util.List; import foundation.e.drive.database.DbHelper; import foundation.e.drive.models.DownloadRequest; +import foundation.e.drive.models.SyncRequest; import foundation.e.drive.models.SyncedFileState; import foundation.e.drive.models.SyncedFolder; import foundation.e.drive.utils.CommonUtils; @@ -34,10 +33,9 @@ public class RemoteContentScanner extends AbstractContentScanner { /** * @param context Context used to access Database, etc. - * @param account Account used to checked if user has change some synchronization's settings */ - public RemoteContentScanner(Context context, Account account, List syncedFolders) { - super(context, account, syncedFolders); + public RemoteContentScanner(Context context, List syncedFolders) { + super(context, syncedFolders); Timber.tag(RemoteContentScanner.class.getSimpleName()); } @@ -74,7 +72,6 @@ public class RemoteContentScanner extends AbstractContentScanner { final SyncedFileState newFileState = new SyncedFileState(-1, fileName, parentDir.getLocalFolder() + fileName, remoteFilePath, file.getEtag(), 0, parentDir.getId(), parentDir.isMediaType(), scannableValue); - //Store it in DB final int storedId = DbHelper.manageSyncedFileStateDB(newFileState, "INSERT", context); if (storedId > 0) { newFileState.setId(storedId); @@ -84,33 +81,8 @@ public class RemoteContentScanner extends AbstractContentScanner { } @Override - protected void onMissingRemoteFile(SyncedFileState fileState) { - if (!CommonUtils.isThisSyncAllowed(account, fileState.isMediaType())) { - Timber.d("Sync of current file: %s isn't allowed", fileState.getName()); - return; - } - - if (!fileState.hasBeenSynchronizedOnce()) { - return; - } - - final File file = new File(fileState.getLocalPath()); - if (!file.exists()) { - return; - } - - context.getContentResolver().delete(MediaStore.Files.getContentUri("external"), - MediaStore.Files.FileColumns.DATA + "=?", - new String[]{CommonUtils.getLocalPath(file)}); - - if (!file.delete()) { //May throw SecurityException or IOException - Timber.d("local file ( %s ) removal failed",file.getName()); - return; - } - - if (DbHelper.manageSyncedFileStateDB(fileState, "DELETE", context) <= 0) { - Timber.e("Failed to remove %s from DB", file.getName()); - } + protected void onMissingFile(SyncedFileState fileState) { + this.syncRequests.put(fileState.getId(), new SyncRequest(fileState, LOCAL_DELETE)); } @Override diff --git a/app/src/main/java/foundation/e/drive/database/SyncedFileStateDAO.java b/app/src/main/java/foundation/e/drive/database/SyncedFileStateDAO.java index 18cff0df548c69b8fb99787783f5a4463c431024..7f14da6521f9a8d10ab2498b310c2aaeec5aea06 100644 --- a/app/src/main/java/foundation/e/drive/database/SyncedFileStateDAO.java +++ b/app/src/main/java/foundation/e/drive/database/SyncedFileStateDAO.java @@ -19,6 +19,7 @@ import android.database.sqlite.SQLiteDoneException; import android.database.sqlite.SQLiteStatement; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import foundation.e.drive.models.SyncedFileState; @@ -195,15 +196,26 @@ import static foundation.e.drive.database.SyncedFileStateContract.SYNCEDFOLDER_I /* package */ List getBySyncedFolderIds(List syncedFolderIds) { final List result = new ArrayList<>(); if (syncedFolderIds == null || syncedFolderIds.isEmpty()) { + Timber.d("getBySyncedFolderIds(): SyncedFOlderIds is empty or null"); return result; } - final String whereClause = SYNCEDFOLDER_ID + " IN (?)"; - final String[] whereValue = new String[] { - syncedFolderIds.toString() - .replace("[", "(") - .replace("]", ")") }; - final Cursor cursor = mDB.query(TABLE_NAME, allColumns, whereClause, whereValue, null, null, null); + final List matcherList = new ArrayList<>(); //list of "?" to be replaced by value by SQliteDatabase.query() call + + final String[] whereValue = new String[syncedFolderIds.size()]; + for (int i = 0; i < syncedFolderIds.size(); i++) { + matcherList.add("?"); + whereValue[i] = syncedFolderIds.get(i).toString(); + } + + final StringBuilder whereClause = new StringBuilder(); + whereClause.append(SYNCEDFOLDER_ID) + .append(" IN ") + .append(matcherList.toString() + .replace("[", "(") + .replace("]", ")")); + + final Cursor cursor = mDB.query(TABLE_NAME, allColumns, whereClause.toString(), whereValue, null, null, null); cursor.moveToFirst(); while(!cursor.isAfterLast() ) { result.add( cursorToSyncedFileState(cursor) ); diff --git a/app/src/main/java/foundation/e/drive/models/SyncRequest.java b/app/src/main/java/foundation/e/drive/models/SyncRequest.java index d2b820c8e310e828d24b05ddb284bff995abf70b..29bab6b9f2c52f20b2edf7eec51ec9fb2c7c60fe 100644 --- a/app/src/main/java/foundation/e/drive/models/SyncRequest.java +++ b/app/src/main/java/foundation/e/drive/models/SyncRequest.java @@ -10,7 +10,7 @@ package foundation.e.drive.models; import androidx.annotation.Nullable; public class SyncRequest { - public enum Type { UPLOAD, DOWNLOAD, REMOTE_DELETE}; + public enum Type { UPLOAD, DOWNLOAD, REMOTE_DELETE, LOCAL_DELETE}; private final SyncedFileState syncedFileState; diff --git a/app/src/main/java/foundation/e/drive/operations/ListFileRemoteOperation.java b/app/src/main/java/foundation/e/drive/operations/ListFileRemoteOperation.java index 803e373c5fb13e0dbeb6ade77768b030a17bd7aa..627f31ff87de57fbdd3644fb695310d25ff7a0fb 100644 --- a/app/src/main/java/foundation/e/drive/operations/ListFileRemoteOperation.java +++ b/app/src/main/java/foundation/e/drive/operations/ListFileRemoteOperation.java @@ -10,6 +10,9 @@ package foundation.e.drive.operations; import android.content.Context; + +import androidx.annotation.VisibleForTesting; + import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.operations.RemoteOperation; import com.owncloud.android.lib.common.operations.RemoteOperationResult; diff --git a/app/src/main/java/foundation/e/drive/services/ObserverService.java b/app/src/main/java/foundation/e/drive/services/ObserverService.java index 0da565c1a5b804f0f2427ef70583b70d07c52f13..3d201f7b2832b1856817d33898f30cb83f64b450 100644 --- a/app/src/main/java/foundation/e/drive/services/ObserverService.java +++ b/app/src/main/java/foundation/e/drive/services/ObserverService.java @@ -315,7 +315,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene syncedFoldersId); if (!remoteFiles.isEmpty() || !syncedFileStates.isEmpty()) { - final RemoteContentScanner scanner = new RemoteContentScanner(getApplicationContext(), mAccount, mSyncedFolders); + final RemoteContentScanner scanner = new RemoteContentScanner(getApplicationContext(), mSyncedFolders); syncRequests.putAll(scanner.scanContent(remoteFiles, syncedFileStates)); } } @@ -404,7 +404,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene folderIdList); if (!syncedFileStates.isEmpty() || !fileList.isEmpty() ) { - final LocalContentScanner scanner= new LocalContentScanner(getApplicationContext(), mAccount, mSyncedFolders); + final LocalContentScanner scanner= new LocalContentScanner(getApplicationContext(), mSyncedFolders); syncRequests.putAll(scanner.scanContent(fileList, syncedFileStates)); } } diff --git a/app/src/main/java/foundation/e/drive/services/SynchronizationService.java b/app/src/main/java/foundation/e/drive/services/SynchronizationService.java index 678e462e02ee95c936d0b723b98e0958a086910b..ed0f2ab9ae1199e1d0ab5c8f22e2ff99eb1fb940 100644 --- a/app/src/main/java/foundation/e/drive/services/SynchronizationService.java +++ b/app/src/main/java/foundation/e/drive/services/SynchronizationService.java @@ -21,7 +21,6 @@ import android.os.HandlerThread; import android.os.IBinder; import androidx.annotation.Nullable; - import com.nextcloud.common.NextcloudClient; import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.operations.OnRemoteOperationListener; @@ -44,12 +43,13 @@ import foundation.e.drive.operations.UploadFileOperation; import foundation.e.drive.utils.AppConstants; import foundation.e.drive.utils.CommonUtils; import foundation.e.drive.utils.DavClientProvider; +import foundation.e.drive.work.LocalFileDeleter; import timber.log.Timber; /** * @author Vincent Bourgmayer */ -public class SynchronizationService extends Service implements OnRemoteOperationListener { +public class SynchronizationService extends Service implements OnRemoteOperationListener, LocalFileDeleter.LocalFileDeletionListener { private final SynchronizationBinder binder = new SynchronizationBinder(); private ConcurrentLinkedDeque syncRequestQueue; private ConcurrentHashMap startedSync; //Integer is thread index (1 to workerAmount) @@ -57,6 +57,7 @@ public class SynchronizationService extends Service implements OnRemoteOperation private Account account; private final int workerAmount = 2; private Thread[] threadPool; + @SuppressWarnings("DeprecatedIsStillUsed") @Deprecated private OwnCloudClient ocClient; private NextcloudClient ncClient; @@ -86,6 +87,7 @@ public class SynchronizationService extends Service implements OnRemoteOperation syncRequestQueue = new ConcurrentLinkedDeque<>(); startedSync = new ConcurrentHashMap<>(); threadPool = new Thread[workerAmount]; + //noinspection deprecation ocClient = DavClientProvider.getInstance().getClientInstance(account, getApplicationContext()); ncClient = DavClientProvider.getInstance().getNcClientInstance(account, getApplicationContext()); @@ -124,6 +126,7 @@ public class SynchronizationService extends Service implements OnRemoteOperation */ public void queueSyncRequest(SyncRequest request) { for (SyncWrapper syncWrapper : startedSync.values()) { + //noinspection EqualsBetweenInconvertibleTypes if (syncWrapper.isRunning() && syncWrapper.equals(request)) { return; } @@ -145,11 +148,12 @@ public class SynchronizationService extends Service implements OnRemoteOperation public void queueSyncRequests(Collection requests) { for (SyncWrapper syncWrapper : startedSync.values()) { if (syncWrapper.isRunning()) { - requests.removeIf(syncRequest -> syncWrapper.equals(syncRequest)); + //noinspection EqualsBetweenInconvertibleTypes + requests.removeIf(syncWrapper::equals); } } - requests.removeIf(syncRequest -> skipBecauseOfPreviousFail(syncRequest)); + requests.removeIf(this::skipBecauseOfPreviousFail); syncRequestQueue.removeAll(requests); syncRequestQueue.addAll(requests); @@ -197,20 +201,34 @@ public class SynchronizationService extends Service implements OnRemoteOperation if (!canStart(threadIndex)) return; final SyncRequest request = this.syncRequestQueue.poll(); //return null if empty - if (request == null) return; + if (request == null + || request.getSyncedFileState() == null + || !CommonUtils.isThisSyncAllowed(account, request.getSyncedFileState().isMediaType())) { + return; + } final SyncWrapper syncWrapper = new SyncWrapper(request, account, getApplicationContext()); - final RemoteOperation operation = syncWrapper.getRemoteOperation(); - if (operation != null) { - if (CommonUtils.isThisSyncAllowed(account, request.getSyncedFileState().isMediaType())) { - CommonUtils.createNotificationChannel(this); - Timber.v(" starts " + request.getSyncedFileState().getName() - + " " + request.getOperationType().name() + " on thread " + threadIndex); - threadPool[threadIndex] = operation.execute(ocClient, this, handler); - startedSync.put(threadIndex, syncWrapper); - } + if (request.getOperationType().equals(SyncRequest.Type.LOCAL_DELETE)) { + final LocalFileDeleter fileDeleter = new LocalFileDeleter(request.getSyncedFileState()); + threadPool[threadIndex] = new Thread(fileDeleter.getRunnable( handler, threadIndex, getApplicationContext(), this)); + threadPool[threadIndex].start(); + startedSync.put(threadIndex, syncWrapper); + return; } + + @SuppressWarnings("rawtypes") + final RemoteOperation operation = syncWrapper.getRemoteOperation(); + + if (operation == null ) return; + + CommonUtils.createNotificationChannel(this); + Timber.v(" starts " + request.getSyncedFileState().getName() + + " " + request.getOperationType().name() + " on thread " + threadIndex); + + //noinspection deprecation + threadPool[threadIndex] = operation.execute(ocClient, this, handler); + startedSync.put(threadIndex, syncWrapper); } /** @@ -222,11 +240,8 @@ public class SynchronizationService extends Service implements OnRemoteOperation final boolean meteredNetworkAllowed = CommonUtils.isMeteredNetworkAllowed(account); final SyncWrapper syncWrapper = startedSync.get(threadIndex); - if ((syncWrapper != null && syncWrapper.isRunning()) - || !CommonUtils.haveNetworkConnection(getApplicationContext(), meteredNetworkAllowed)) { - return false; - } - return true; + return (syncWrapper == null || !syncWrapper.isRunning()) + && CommonUtils.haveNetworkConnection(getApplicationContext(), meteredNetworkAllowed); } @Override @@ -313,6 +328,16 @@ public class SynchronizationService extends Service implements OnRemoteOperation } } + @Override + public void onDeletionComplete(int threadId, boolean succeed) { + final SyncWrapper wrapper = startedSync.get(threadId); + if (wrapper != null) { + Timber.d(wrapper.getRequest().getSyncedFileState().getLocalPath() + " deletion result: " + succeed); + wrapper.setRunning(false); + } + startWorker(threadId); + } + public class SynchronizationBinder extends Binder{ public SynchronizationService getService(){ return SynchronizationService.this; diff --git a/app/src/main/java/foundation/e/drive/utils/FileDiffUtils.java b/app/src/main/java/foundation/e/drive/utils/FileDiffUtils.java index 3e9410fefe819e42eda70111d630f6d84acc2370..7b5c1814bc6727fe66ba0a35620b43b384895ea3 100644 --- a/app/src/main/java/foundation/e/drive/utils/FileDiffUtils.java +++ b/app/src/main/java/foundation/e/drive/utils/FileDiffUtils.java @@ -90,6 +90,7 @@ public class FileDiffUtils { */ private static boolean isRemoteSizeSameAsLocalSize(RemoteFile remoteFile, File localFile) { // if local file doesn't exist its size will be 0 - return remoteFile.getLength() == localFile.length(); + // remoteFile.getSize() : getSize() is equal to getLength() except that for folder is also sum the content of the folder! + return remoteFile.getSize() == localFile.length(); } } diff --git a/app/src/main/java/foundation/e/drive/work/LocalFileDeleter.java b/app/src/main/java/foundation/e/drive/work/LocalFileDeleter.java new file mode 100644 index 0000000000000000000000000000000000000000..f617855185af86fe939b22f5975b8080f5fbb2c1 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/work/LocalFileDeleter.java @@ -0,0 +1,73 @@ +/* + * Copyright © MURENA SAS 2022. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ +package foundation.e.drive.work; + +import android.content.Context; +import android.os.Handler; +import android.provider.MediaStore; + +import java.io.File; + +import foundation.e.drive.database.DbHelper; +import foundation.e.drive.models.SyncedFileState; +import foundation.e.drive.utils.CommonUtils; +import timber.log.Timber; + +/** + * This worker is called when a remote file has been detected as removed and that all the + * scanning process lead to choose to delete the local version too. + * @author vincent Bourgmayer + */ +public class LocalFileDeleter { + + public interface LocalFileDeletionListener { + void onDeletionComplete(int threadId, boolean succeed); + } + + private final SyncedFileState fileState; + + public LocalFileDeleter(SyncedFileState syncedFileState) { + fileState = syncedFileState; + } + + public Runnable getRunnable(final Handler handler, final int threadId, final Context context, final LocalFileDeletionListener listener) { + return () -> { + final boolean succeed = deleteFile(fileState, context); + notifyCompletion(threadId, listener, succeed, handler); + }; + } + + private boolean deleteFile(SyncedFileState fileState, Context context) { + final File file = new File(fileState.getLocalPath()); + if (file.exists()) { + try { + if (!file.delete()) { + Timber.w("local file ( %s ) removal failed", file.getName()); + return false; + } + } catch (SecurityException exception) { + Timber.e(exception); + return false; + } + + context.getContentResolver().delete(MediaStore.Files.getContentUri("external"), + MediaStore.Files.FileColumns.DATA + "=?", + new String[]{CommonUtils.getLocalPath(file)}); + } + + + if (DbHelper.manageSyncedFileStateDB(fileState, "DELETE", context) <= 0) { + Timber.e("Failed to remove %s from DB", file.getName()); + } + return true; + } + + private void notifyCompletion(final int threadId, final LocalFileDeletionListener listener, final boolean success, final Handler handler) { + handler.post(() -> listener.onDeletionComplete(threadId, success)); + } +} diff --git a/app/src/test/java/foundation/e/drive/contentScanner/LocalContentScannerTest.java b/app/src/test/java/foundation/e/drive/contentScanner/LocalContentScannerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..118dd6a24bd509888f0191afd49ba06959414f11 --- /dev/null +++ b/app/src/test/java/foundation/e/drive/contentScanner/LocalContentScannerTest.java @@ -0,0 +1,145 @@ +/* + * Copyright © MURENA SAS 2022. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ + +package foundation.e.drive.contentScanner; + +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.os.Build; + +import androidx.test.core.app.ApplicationProvider; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowLog; + +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import foundation.e.drive.models.SyncRequest; +import foundation.e.drive.models.SyncedFileState; +import foundation.e.drive.models.SyncedFolder; + +/** + * @author vincent Bourgmayer + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Build.VERSION_CODES.O, manifest = Config.NONE) +public class LocalContentScannerTest { + + private final Context context; + private LocalContentScanner scannerUnderTest; + + public LocalContentScannerTest() { + context = ApplicationProvider.getApplicationContext(); + ShadowLog.stream = System.out; + } + + @Before + public void setUp() { + final File folder = new File("local/path/"); + final List directoryList = new ArrayList<>(); + // Without the + "/" for local path of SyncedFolder, some test will fails because folder.getAbsolutePath() doesn't have "/" at the end + //but syncedFolders in database (in production) has the path separator at the end of the local path + directoryList.add(new SyncedFolder("Photos", folder.getAbsolutePath() + "/", "remote/path/", true, true, true, true)); + scannerUnderTest = new LocalContentScanner(context, directoryList); + } + + @Test + public void scanContent_withMissingLocalFiles_returnListWithRemoteDeletionRequest() { + final List localFiles = new ArrayList<>(); + + final List syncedFileStates = new ArrayList<>(); + final SyncedFileState syncedFileState1 = new SyncedFileState(1, "titi.png", "local/path/titi.png", "remote/path/titi.png", "aaaa", 123456, 0, true, 3); + final SyncedFileState syncedFileState2 = new SyncedFileState(2, "tutu.png", "local/path/tutu.png", "remote/path/tutu.png", "bbbb", 234567, 0, true, 3); + + syncedFileStates.add(syncedFileState1); + syncedFileStates.add(syncedFileState2); + + final HashMap result = scannerUnderTest.scanContent( localFiles, syncedFileStates); + Assert.assertEquals("Result is expected to be a list with 2 UploadRequest but has : " + result.size() + "elements", 2, result.size()); + Assert.assertTrue("Each SyncRequests should be upload request but some were not", result.values().stream().allMatch(request -> request.getOperationType().equals(SyncRequest.Type.REMOTE_DELETE))); + } + + @Test + public void scanContent_withLocalFilesUnsynced_returnListWithUploadRequests() { + final List localFiles = new ArrayList<>(); + final File file1 = Mockito.spy(new File("local/path/titi.png")); + when(file1.lastModified()).thenReturn(987654L); + final File file2 = Mockito.spy(new File("local/path/tutu.png")); + when(file2.lastModified()).thenReturn(654321L); + + localFiles.add(file1); + localFiles.add(file2); + + final List syncedFileStates = new ArrayList<>(); + final SyncedFileState syncedFileState1 = new SyncedFileState(1, file1.getName(), file1.getAbsolutePath(), "remote/path/titi.png", "", 0, 0, true, 3); + final SyncedFileState syncedFileState2 = new SyncedFileState(2, file2.getName(), file2.getAbsolutePath(), "remote/path/tutu.png", "", 0, 0, true, 3); + + syncedFileStates.add(syncedFileState1); + syncedFileStates.add(syncedFileState2); + + final HashMap result = scannerUnderTest.scanContent( localFiles, syncedFileStates); + Assert.assertEquals("Result is expected to be a list with 2 UploadRequest but has : " + result.size() + "elements", 2, result.size()); + Assert.assertTrue("Each SyncRequests should be upload request but some were not", result.values().stream().allMatch(request -> request.getOperationType().equals(SyncRequest.Type.UPLOAD))); + } + + + @Test + public void scanContent_withUpdatedLocalFiles_returnListWithUploadRequests() { + final List localFiles = new ArrayList<>(); + final File file1 = Mockito.spy(new File("local/path/titi.png")); + when(file1.lastModified()).thenReturn(987654L); + final File file2 = Mockito.spy(new File("local/path/tutu.png")); + when(file2.lastModified()).thenReturn(654321L); + + localFiles.add(file1); + localFiles.add(file2); + + final List syncedFileStates = new ArrayList<>(); + final SyncedFileState syncedFileState1 = new SyncedFileState(1, file1.getName(), file1.getAbsolutePath(), "remote/path/titi.png", "aaaa", 123456, 0, true, 3); + final SyncedFileState syncedFileState2 = new SyncedFileState(2, file2.getName(), file2.getAbsolutePath(), "remote/path/tutu.png", "bbbb", 234567, 0, true, 3); + + syncedFileStates.add(syncedFileState1); + syncedFileStates.add(syncedFileState2); + + final HashMap result = scannerUnderTest.scanContent( localFiles, syncedFileStates); + Assert.assertEquals("Result is expected to be a list with 2 UploadRequest but has : " + result.size() + "elements", 2, result.size()); + Assert.assertTrue("Each SyncRequests should be upload request but some were not", result.values().stream().allMatch(request -> request.getOperationType().equals(SyncRequest.Type.UPLOAD))); + } + + @Test + public void scanContent_withNewLocalFiles_returnListWithUploadRequests() { + final List localFiles = new ArrayList<>(); + final File newFile1 = new File("local/path/titi.png"); + final File newFile2 = new File("local/path/tutu.png"); + localFiles.add(newFile1); + localFiles.add(newFile2); + + final List syncedFileStates = new ArrayList<>(); + final HashMap result = scannerUnderTest.scanContent( localFiles, syncedFileStates); + Assert.assertEquals("Result is expected to be a list with 2 UploadRequest but has : " + result.size() + "elements", 2, result.size()); + Assert.assertTrue("Each SyncRequests should be upload request but some were not", result.values().stream().allMatch(request -> request.getOperationType().equals(SyncRequest.Type.UPLOAD))); + } + + @Test + public void scanContent_emptyLists_returnEmptyList() { + final List localFiles = new ArrayList<>(); + final List syncedFileStates = new ArrayList<>(); + final HashMap result = scannerUnderTest.scanContent( localFiles, syncedFileStates); + Assert.assertTrue("Result is expected to be an empty list but contained: " + result.size(), result.isEmpty()); + } +} diff --git a/app/src/test/java/foundation/e/drive/contentScanner/RemoteContentScannerTest.java b/app/src/test/java/foundation/e/drive/contentScanner/RemoteContentScannerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ae291d142ad98c460f753647c765780cf0b10e85 --- /dev/null +++ b/app/src/test/java/foundation/e/drive/contentScanner/RemoteContentScannerTest.java @@ -0,0 +1,212 @@ +/* + * Copyright © MURENA SAS 2022. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the GNU Public License v3.0 + * which accompanies this distribution, and is available at + * http://www.gnu.org/licenses/gpl.html + */ +package foundation.e.drive.contentScanner; + +import static org.junit.Assert.assertEquals; + +import android.content.Context; +import android.os.Build; + +import androidx.test.core.app.ApplicationProvider; + +import com.owncloud.android.lib.resources.files.model.RemoteFile; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; +import org.robolectric.shadows.ShadowLog; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import foundation.e.drive.TestUtils; +import foundation.e.drive.database.DbHelper; +import foundation.e.drive.models.SyncRequest; +import foundation.e.drive.models.SyncedFileState; +import foundation.e.drive.models.SyncedFolder; + +/** + * @author vincent Bourgmayer + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Build.VERSION_CODES.O, manifest = Config.NONE) +public class RemoteContentScannerTest { + + private final Context context; + private RemoteContentScanner scannerUnderTest; + + public RemoteContentScannerTest() { + context = ApplicationProvider.getApplicationContext(); + ShadowLog.stream = System.out; + } + + @Before + public void setUp() { + prepareDB(); + final List directoryList = new ArrayList<>(); + directoryList.add(new SyncedFolder("Photos", "local/path/", "remote/path/", true, true, true, true)); + scannerUnderTest = new RemoteContentScanner(context, directoryList); + } + + private void prepareDB() { + DbHelper.insertSyncedFolder(createSyncedFolder("small"), context); + DbHelper.insertSyncedFolder(createSyncedFolder("medium"), context); + DbHelper.insertSyncedFolder(createSyncedFolder("large"), context); + + //assertion for debug purpose + assertEquals("There isn't three folder in DB as expected", 3, DbHelper.getSyncedFolderList(context, true).size()); + } + + /** + * Create the syncedFolder instance + * @param name the name of the folder + * @return SyncedFolder instance + */ + private SyncedFolder createSyncedFolder(String name) { + return new SyncedFolder(name, + TestUtils.TEST_LOCAL_ROOT_FOLDER_PATH+name+"/", + TestUtils.TEST_REMOTE_ROOT_FOLDER_PATH+name+"/", + true, true, true, true); + } + + @Test + public void scanContent_withoutRemoteContent_returnListWithLocalDeleteRequest() { + final List cloudContent = new ArrayList<>(); + Assert.assertTrue("Fake list of cloud Content is not empty", cloudContent.isEmpty()); + + final List dbContent = new ArrayList<>(); + dbContent.add(new SyncedFileState(5, "toto", "local/path/toto", "remote/path/toto", "5555", 22222, 2, true, 3)); + dbContent.add(new SyncedFileState(3, "titi", "local/path/titi", "remote/path/titi", "5556", 22322, 2, true, 3)); + dbContent.add(new SyncedFileState(2, "tata", "local/path/tata", "remote/path/tata", "5557", 22232, 2, true, 3)); + + Assert.assertFalse("List of SyncedFileState is empty", dbContent.isEmpty()); + + final HashMap scanResult = scannerUnderTest.scanContent(cloudContent, dbContent); + + Assert.assertEquals("scanResult's size doesn't match the expected result", 3, scanResult.size()); + + for (SyncRequest request : scanResult.values()) { + Assert.assertEquals("SyncRequest's type should be LOCAL_DELETE but is "+request.getOperationType(), SyncRequest.Type.LOCAL_DELETE, request.getOperationType()); + } + } + + private List generateListOfNewRemoteFile() { + final ArrayList result = new ArrayList<>(); + + final RemoteFile newFile1 = Mockito.mock(RemoteFile.class); + Mockito.when(newFile1.getRemotePath()).thenReturn("remote/path/tutu"); + Mockito.when(newFile1.getEtag()).thenReturn("33323"); + + + final RemoteFile newFile2 = Mockito.mock(RemoteFile.class); + Mockito.when(newFile2.getRemotePath()).thenReturn("remote/path/tete"); + Mockito.when(newFile2.getEtag()).thenReturn("33423"); + + + result.add(newFile1); + result.add(newFile2); + + return result; + } + + @Test + public void scanContent_withUpdatedRemoteContent_returnListWithDownloadRequest() { + final List cloudContent = new ArrayList<>(); + + final RemoteFile updatedFile1 = Mockito.mock(RemoteFile.class); + Mockito.when(updatedFile1.getRemotePath()).thenReturn("remote/path/toto"); + Mockito.when(updatedFile1.getEtag()).thenReturn("33423"); + Mockito.when(updatedFile1.getSize()).thenReturn(12l); + + final RemoteFile updatedFile2 = Mockito.mock(RemoteFile.class); + Mockito.when(updatedFile2.getRemotePath()).thenReturn("remote/path/titi"); + Mockito.when(updatedFile2.getEtag()).thenReturn("34523"); + Mockito.when(updatedFile2.getSize()).thenReturn(14l); + + final RemoteFile uptodateFile1 = Mockito.mock(RemoteFile.class); + Mockito.when(uptodateFile1.getRemotePath()).thenReturn("remote/path/tata"); + Mockito.when(uptodateFile1.getEtag()).thenReturn("5557"); + + cloudContent.add(uptodateFile1); + cloudContent.add(updatedFile1); + cloudContent.add(updatedFile2); + + Assert.assertEquals("Expected cloudContent size: 3 but got : " + cloudContent.size(), 3, cloudContent.size()); + + + final List dbContent = new ArrayList<>(); + dbContent.add(new SyncedFileState(5, "toto", "local/path/toto", "remote/path/toto", "5555", 22222, 2, true, 3)); + dbContent.add(new SyncedFileState(3, "titi", "local/path/titi", "remote/path/titi", "5556", 22322, 2, true, 3)); + dbContent.add(new SyncedFileState(2, "tata", "local/path/tata", "remote/path/tata", "5557", 22232, 2, true, 3)); + + Assert.assertFalse("List of SyncedFileState is empty", dbContent.isEmpty()); + + final HashMap scanResult = scannerUnderTest.scanContent(cloudContent, dbContent); + Assert.assertEquals("scanResult's size doesn't match the expected result", 2, scanResult.size()); + + for (SyncRequest request : scanResult.values()) { + Assert.assertEquals("SyncRequest's type should be DOWNLOAD but is "+request.getOperationType(), SyncRequest.Type.DOWNLOAD, request.getOperationType()); + } + } + + @Test + public void scanContent_withNewRemoteContent_returnListWithDownloadRequest() { + final List cloudContent = generateListOfNewRemoteFile(); + + final RemoteFile updatedFile1 = Mockito.mock(RemoteFile.class); + Mockito.when(updatedFile1.getRemotePath()).thenReturn("remote/path/toto"); + Mockito.when(updatedFile1.getEtag()).thenReturn("5555"); + + final RemoteFile updatedFile2 = Mockito.mock(RemoteFile.class); + Mockito.when(updatedFile2.getRemotePath()).thenReturn("remote/path/titi"); + Mockito.when(updatedFile2.getEtag()).thenReturn("5556"); + + final RemoteFile uptodateFile1 = Mockito.mock(RemoteFile.class); + Mockito.when(uptodateFile1.getRemotePath()).thenReturn("remote/path/tata"); + Mockito.when(uptodateFile1.getEtag()).thenReturn("5557"); + + cloudContent.add(uptodateFile1); + cloudContent.add(updatedFile1); + cloudContent.add(updatedFile2); + + Assert.assertEquals("Expected cloudContent size: 5 but got : " + cloudContent.size(), 5, cloudContent.size()); + + + final List dbContent = new ArrayList<>(); + dbContent.add(new SyncedFileState(5, "toto", "local/path/toto", "remote/path/toto", "5555", 22222, 2, true, 3)); + dbContent.add(new SyncedFileState(3, "titi", "local/path/titi", "remote/path/titi", "5556", 22322, 2, true, 3)); + dbContent.add(new SyncedFileState(2, "tata", "local/path/tata", "remote/path/tata", "5557", 22232, 2, true, 3)); + + Assert.assertFalse("List of SyncedFileState is empty", dbContent.isEmpty()); + + final HashMap scanResult = scannerUnderTest.scanContent(cloudContent, dbContent); + Assert.assertEquals("scanResult's size doesn't match the expected result", 2, scanResult.size()); + + for (SyncRequest request : scanResult.values()) { + Assert.assertEquals("SyncRequest's type should be DOWNLOAD but is "+request.getOperationType(), SyncRequest.Type.DOWNLOAD, request.getOperationType()); + } + } + + @Test + public void scanContent_emptyLists_returnEmptyList() { + final List cloudContent = new ArrayList<>(); + final List dbContent = new ArrayList<>(); + + final HashMap scanResult = scannerUnderTest.scanContent(cloudContent, dbContent); + Assert.assertEquals("scanResult's size doesn't match the expected result", 0, scanResult.size()); + + for (SyncRequest request : scanResult.values()) { + System.out.println(request.getOperationType() + "for " +request.getSyncedFileState().getRemotePath()); + } + } +} \ No newline at end of file diff --git a/app/src/test/java/foundation/e/drive/database/SyncedFileStateDAOTest.java b/app/src/test/java/foundation/e/drive/database/SyncedFileStateDAOTest.java index 945b8694edbeba55c7049a3b2e3983b04c9e0b56..a954e22ba2f3de51ac885f0d146d7a3b93343324 100644 --- a/app/src/test/java/foundation/e/drive/database/SyncedFileStateDAOTest.java +++ b/app/src/test/java/foundation/e/drive/database/SyncedFileStateDAOTest.java @@ -67,11 +67,8 @@ public class SyncedFileStateDAOTest { Assert.assertTrue(result.isEmpty()); } - /* - * @todo find why this test fails - */ + @Test - @Ignore public void getBySyncedFolderIds_notEmpty_returnOneFileState() { final List folderIds = new ArrayList<>(); folderIds.add(new Long(12));