diff --git a/app/src/main/java/foundation/e/drive/contentScanner/AbstractFileLister.java b/app/src/main/java/foundation/e/drive/contentScanner/AbstractFileLister.java new file mode 100644 index 0000000000000000000000000000000000000000..1f4284cdc80a89c53f59f7aefdee4e1a523d5c59 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/contentScanner/AbstractFileLister.java @@ -0,0 +1,221 @@ +/* + * Copyright © ECORP 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 android.content.Context; + +import androidx.annotation.NonNull; + +import com.owncloud.android.lib.resources.files.FileUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; + +import foundation.e.drive.database.DbHelper; +import foundation.e.drive.models.SyncedFolder; +import timber.log.Timber; + +/** + * Factorisation of codes used to list file on cloud and on device + * Implementation will vary depending of it is RemoteFile or File instance + * that we want to list + * @author vincent Bourgmayer + */ +public abstract class AbstractFileLister { + + /** + * Check if the given syncedFolder can be skip + * @param syncedFolder The syncedFolder instance + * @return true if we can skip + */ + protected abstract boolean skipSyncedFolder(@NonNull SyncedFolder syncedFolder); + + /** + * Check if the file can be skip or if it should be added to the list to scan + * @param file file Object to check + * @return true if file must be ignored + */ + protected abstract boolean skipFile(@NonNull T file); + + /** + * If the given folder has not changed, compared to database's data, skip it + * @param currentDirectory The real latest state of the directory + * @param syncedFolder The last known state of the directory + * @param context Context used to access database, to look for unsynced file for the given directory + * @return true if we can skip this directory + */ + protected abstract boolean skipDirectory(@NonNull T currentDirectory, @NonNull SyncedFolder syncedFolder, @NonNull Context context); + protected abstract boolean isDirectory(@NonNull T file); + protected abstract String getFileName(@NonNull T file); + protected abstract FolderLoader createFolderLoader(); + protected abstract void updateSyncedFolder(@NonNull SyncedFolder syncedFolder,@NonNull T folder); + + /** + * This allows to use RemoteOperationResult for RemoteFileLister + * todo: some optimization could be done with FolderWrapper + * @param RemoteFile or File + */ + /* package */ interface FolderLoader { + FolderWrapper getFolderWrapper(); + boolean load(@NonNull SyncedFolder folder); + } + + protected final List folders; + private final List contentToScan; + protected boolean diveIntoSkippedDir = false; //to be overriden only + + /** + * constructor to be call with super + * @param folders List of SyncedFolder to read + */ + protected AbstractFileLister(@NonNull List folders) { + this.folders = folders; + this.contentToScan = new ArrayList<>(); + } + + /** + * Perform the listing of files + * @param context + * @return + */ + public boolean listContentToScan(@NonNull Context context) { + final ListIterator iterator = folders.listIterator() ; + boolean isSomeContentToSync = false; + while (iterator.hasNext()) { + final SyncedFolder syncedFolder = iterator.next(); + if (syncedFolder == null) continue; + Timber.v("SyncedFolder : %s, %s, %s, %s, %s", syncedFolder.getLibelle(), syncedFolder.getLocalFolder(), syncedFolder.getLastModified(), syncedFolder.isScanLocal(), syncedFolder.getId()); + isSomeContentToSync = hasDirectoryContentToScan(syncedFolder, iterator, context) || isSomeContentToSync; + } + + if (isSomeContentToSync) { + DbHelper.updateSyncedFolders(folders, context); //@todo: maybe do this when all contents will be synced. + } + return isSomeContentToSync; + } + + /** + * Detailed process for reading a single folder + * @param folder the SyncedFolder to check + * @param iterator iterator over list of SyncedFolder + * @param context context used to call DB + * @return true the directory has content to synchronized false otherwise + */ + private boolean hasDirectoryContentToScan(@NonNull SyncedFolder folder, @NonNull ListIterator iterator, @NonNull Context context) { + + final FolderLoader dirLoader = createFolderLoader(); + if (skipSyncedFolder(folder) + || !isSyncedFolderInDb(folder, context) + || !dirLoader.load(folder)) { // I try to get the real directory (File or RemoteFile). + iterator.remove(); + return false; + } + + final FolderWrapper currentDirectory = dirLoader.getFolderWrapper(); + + if (currentDirectory.isMissing()) { + return true; + } + + if (skipDirectory(currentDirectory.getFolder(), folder, context)) { + iterator.remove(); + folder.setToSync(false); + /** + * LocalFileLister need to be able to access subFiles even for skipped directory + * because A folder's lastModified value won't change if a subfolder's content is changed, removed or created + * RemoteFileLister is based on eTag, which change even if a subfolder's content is changed + */ + if (!diveIntoSkippedDir) return false; + } else { + folder.setToSync(true); + } + updateSyncedFolder(folder, currentDirectory.getFolder()); + + //todo: look where to put in subclasses : syncedFolder.setLastEtag(directory.getEtag()).setToSync(true); + + final List dirContent = currentDirectory.getContent(); + if (dirContent != null && !dirContent.isEmpty()) { + contentToScan.addAll(sortContent(iterator, folder, dirContent)); + } + return true; + } + + + /** + * Tell if the given syncedFolder has been persisted in database. + * If it's not the case initally, it tries to persist it. + * @param folder SyncedFolder to check for persistance + * @param context context to contact database + * @return false if not persisted in db + */ + private boolean isSyncedFolderInDb(@NonNull SyncedFolder folder, @NonNull Context context) { + if (folder.getId() > 0) return true; + + final int syncedFolderId = (int) DbHelper.insertSyncedFolder(folder, context); //It will return -1 if there is an error, like an already existing folder with same value + if (syncedFolderId <= 0) { + Timber.v("insertion of syncedFolder for %s failed: %s ", folder.getRemoteFolder(), syncedFolderId); + return false; + } + + folder.setId(syncedFolderId); + return true; + } + + + /** + * Split content of a folder: subfolder on one side are added into the iterator loop + * while subfiles are added to result list + * @param iterator iterator intance over list of SyncedFolder + * @param folder the SyncFolder which own the content + * @param dirContent Content to sort + * @return List of subfiles to scan or empty list if nothing + */ + private List sortContent(@NonNull ListIterator iterator, @NonNull SyncedFolder folder, List dirContent) { + final List result = new ArrayList<>(); + if (dirContent == null) return result; + + for (T file : dirContent) { + if (file == null) continue; + + final String fileName = getFileName(file); + if (fileName == null) continue; + + if (isDirectory(file)) { + final SyncedFolder subSyncedFolder = new SyncedFolder(folder, fileName + FileUtils.PATH_SEPARATOR, 0L, "");//Need to set lastModified to 0 to handle it on next iteration + iterator.add(subSyncedFolder); + iterator.previous(); + } else if (folder.isToSync() && !skipFile(file)) { + Timber.v("added %s into list of file to scan", fileName); + result.add(file); + } + } + + return result; + } + + /** + * List of file to scan + * @return List or List is expected based on implementations + */ + public List getContentToScan() { + return contentToScan; + } + + /** + * Share the list of syncedFolder's ID for syncedFolder which has content to scan + * @return List of syncedFolder ids + */ + public List getSyncedFoldersId() { + final List result = new ArrayList<>(); + for (SyncedFolder folder : folders) { + result.add((long) folder.getId()); + } + return result; + } +} diff --git a/app/src/main/java/foundation/e/drive/contentScanner/FolderWrapper.java b/app/src/main/java/foundation/e/drive/contentScanner/FolderWrapper.java new file mode 100644 index 0000000000000000000000000000000000000000..a1a03caf3838a96df8d02f28229259f24c81bb1b --- /dev/null +++ b/app/src/main/java/foundation/e/drive/contentScanner/FolderWrapper.java @@ -0,0 +1,67 @@ +/* + * Copyright © ECORP 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 androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.List; + +/** + * This wrapper is used to contains either a File/RemoteFile instance with its content + * or a "missing file" information. The explanation of this, is because scanning remote file + * may result in different type of error while loading content (network issues, etc.) + * but we want to handle 404 error (= missing resources) has to be considered like a + * potential file deletions. There is probably something better to do for that + * @author vincent Bourgmayer + * @param File or RemoteFile expected + */ +public class FolderWrapper { + + private final T folder; + private final List content; + + protected FolderWrapper(@NonNull T folder) { + content = new ArrayList<>(); + this.folder = folder; + } + + protected FolderWrapper() { + folder = null; + content = null; + } + + /** + * Get the folder (should be RemoteFile or File instance ) represented by this abstraction + * @return null if it is missing + */ + public T getFolder() { + return folder; + } + + /** + * Get subfiles (including sub folder) of the current folder + * @return Null if the directory is missing + */ + public List getContent() { + return content; + } + + public void addContent(@NonNull List newContent) { + content.addAll(newContent); + } + + /** + * Should return true if local File for directory doesn't exists, or if ReadFolderRemoteOperation return 404 + * for a remote file + * @return + */ + public boolean isMissing() { + return folder == null; + } +} diff --git a/app/src/main/java/foundation/e/drive/contentScanner/LocalFileLister.java b/app/src/main/java/foundation/e/drive/contentScanner/LocalFileLister.java new file mode 100644 index 0000000000000000000000000000000000000000..934718ac7b080f74c8071f780d9b40832df7fc59 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/contentScanner/LocalFileLister.java @@ -0,0 +1,113 @@ +/* + * Copyright © ECORP 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 android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import java.io.File; +import java.io.FileFilter; +import java.util.Arrays; +import java.util.List; + +import foundation.e.drive.database.DbHelper; +import foundation.e.drive.fileFilters.FileFilterFactory; +import foundation.e.drive.models.SyncedFolder; +import timber.log.Timber; + +/** + * Implementation of AbstractFileLister with method adapted to local content + * @author vincent Bourgmayer + */ +public class LocalFileLister extends AbstractFileLister { + + public LocalFileLister(@NonNull List directories) { + super(directories); + super.diveIntoSkippedDir = true; + } + + @Override + protected boolean skipDirectory(@NonNull File currentDirectory, @NonNull SyncedFolder syncedFolder, @NonNull Context context) { + return currentDirectory.lastModified() == syncedFolder.getLastModified() + && !DbHelper.syncedFolderHasContentToUpload(syncedFolder.getId(), context); + } + + @Override + protected boolean skipSyncedFolder(@NonNull SyncedFolder syncedFolder) { + final File folder = new File(syncedFolder.getLocalFolder()); + return (syncedFolder.isMediaType() && folder.isHidden()) || !syncedFolder.isScanLocal(); + } + + @Override + protected boolean skipFile(@NonNull File file) { + return file.isHidden(); + } + + @Override + protected boolean isDirectory(@NonNull File file) { + return file.isDirectory(); + } + + @Override + protected String getFileName(@NonNull File file) { + return file.getName(); + } + + @Override + protected LocalFolderLoader createFolderLoader() { + return new LocalFolderLoader(); + } + + @Override + protected void updateSyncedFolder(@NonNull SyncedFolder syncedFolder, @NonNull File folder) { + syncedFolder.setLastModified(folder.lastModified()); + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public class LocalFolderLoader implements FolderLoader { + private FolderWrapper folder; + + @Override + public FolderWrapper getFolderWrapper() { + return folder; + } + + + /** + * "Load" a directory to check if it exist, and list its content + * It is expected to always return true. Its remote equivalent (in RemoteFileLister) + * Might return false because of network, & RemoteFile classes. But that's not the case + * here. + * @param syncedFolder SyncedFolder instance with data about the folder to load + * @return always true because it's about "File" instance. + */ + @Override + public boolean load(@NonNull SyncedFolder syncedFolder) { + final File dir = new File(syncedFolder.getLocalFolder()); + Timber.v("Local Folder (last modified / exists): %s, %s", dir.lastModified(), dir.exists()); + + if (!dir.exists()) { + folder = new FolderWrapper(); //missing Folder Wrapper + return true; + } + + folder = new FolderWrapper(dir); + final String category = syncedFolder.isMediaType() ? "media" : syncedFolder.getLibelle(); + + final FileFilter filter = FileFilterFactory.getFileFilter(category); + final File[] files = dir.listFiles(filter); + if (files != null) { + folder.addContent(Arrays.asList(files)); + } + + return true; + } + } +} diff --git a/app/src/main/java/foundation/e/drive/contentScanner/RemoteFileLister.java b/app/src/main/java/foundation/e/drive/contentScanner/RemoteFileLister.java new file mode 100644 index 0000000000000000000000000000000000000000..440548e99764b086cfbbef0e4800d5031a739d01 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/contentScanner/RemoteFileLister.java @@ -0,0 +1,134 @@ +/* + * Copyright © ECORP 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 android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; + +import com.owncloud.android.lib.common.OwnCloudClient; +import com.owncloud.android.lib.common.operations.RemoteOperationResult; +import com.owncloud.android.lib.resources.files.ReadFolderRemoteOperation; +import com.owncloud.android.lib.resources.files.model.RemoteFile; + +import java.util.ArrayList; +import java.util.List; + +import foundation.e.drive.database.DbHelper; +import foundation.e.drive.models.SyncedFolder; +import foundation.e.drive.utils.CommonUtils; + + +/** + * Implementation of AbstractFileLister with method adapted to remote content + * @author vincent Bourgmayer + */ +public class RemoteFileLister extends AbstractFileLister { + private OwnCloudClient client; + + public RemoteFileLister(@NonNull List directories, @NonNull OwnCloudClient client) { + super(directories); + this.client = client; + } + + @Override + protected boolean skipSyncedFolder(@NonNull SyncedFolder folder) { + return (folder.isMediaType() + && CommonUtils.getFileNameFromPath(folder.getRemoteFolder()).startsWith(".")) + || !folder.isScanRemote(); + } + + @Override + protected boolean skipFile(@NonNull RemoteFile file) { + final String fileName = CommonUtils.getFileNameFromPath(file.getRemotePath()); + return fileName == null || fileName.isEmpty() || fileName.startsWith("."); + } + + @Override + protected boolean skipDirectory(@NonNull RemoteFile directory, @NonNull SyncedFolder syncedFolder, @NonNull Context context) { + return directory.getEtag().equals(syncedFolder.getLastEtag()) + && !DbHelper.syncedFolderHasContentToDownload(syncedFolder.getId(), context); + } + + @Override + protected boolean isDirectory(@NonNull RemoteFile file) { + return file.getMimeType().equals("DIR"); + } + + @Override + protected String getFileName(@NonNull RemoteFile file) { + return CommonUtils.getFileNameFromPath(file.getRemotePath()); + } + + @Override + protected RemoteFolderLoader createFolderLoader() { + return new RemoteFolderLoader(); + } + + @Override + protected void updateSyncedFolder(@NonNull SyncedFolder syncedFolder, @NonNull RemoteFile folder) { + syncedFolder.setLastEtag(folder.getEtag()); + } + + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public class RemoteFolderLoader implements FolderLoader { + private static final int HTTP_404 = 404; + private FolderWrapper directory; + + @Override + public boolean load(@NonNull SyncedFolder syncedFolder) { + if (syncedFolder.getRemoteFolder() == null) return false; + final RemoteOperationResult remoteOperationResult = readRemoteFolder(syncedFolder.getRemoteFolder(), client); + + if (!remoteOperationResult.isSuccess()) { + if (remoteOperationResult.getHttpCode() == HTTP_404) { + directory = new FolderWrapper<>(); + return true; + } + return false; + } + + final List datas = remoteOperationResult.getData(); + if (datas == null || datas.size() < 1) return false; + + final int dataSize = datas.size(); + final RemoteFile remoteDir = (RemoteFile) datas.get(0); + if (remoteDir == null) return false; + + directory = new FolderWrapper<>(remoteDir); + + final List subFiles = new ArrayList<>(); + for (Object o: datas.subList(1, dataSize)) { + final RemoteFile subFile = (RemoteFile) o; + if (subFile != null) { + subFiles.add(subFile); + } + } + directory.addContent(subFiles); + return true; + } + + /** + * Execute the Propfind query to list files under a directory + * @param remotePath RemotePath of the directory to observe + * @param client Client used to execute query + * @return RemoteOperationResult + */ + @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) + public RemoteOperationResult readRemoteFolder(@NonNull String remotePath, @NonNull OwnCloudClient client) { + final ReadFolderRemoteOperation operation = new ReadFolderRemoteOperation(remotePath); + return operation.execute(client); + } + + @Override + public FolderWrapper getFolderWrapper() { + return directory; + } + } +} 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 9463e43bb03d0b8a015bb67ba6aa001a2e258d1d..803e373c5fb13e0dbeb6ade77768b030a17bd7aa 100644 --- a/app/src/main/java/foundation/e/drive/operations/ListFileRemoteOperation.java +++ b/app/src/main/java/foundation/e/drive/operations/ListFileRemoteOperation.java @@ -19,6 +19,8 @@ import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.ListIterator; + +import foundation.e.drive.contentScanner.RemoteFileLister; import foundation.e.drive.database.DbHelper; import foundation.e.drive.models.SyncedFolder; import foundation.e.drive.utils.CommonUtils; @@ -30,19 +32,17 @@ import timber.log.Timber; * @author Vincent Bourgmayer */ public class ListFileRemoteOperation extends RemoteOperation> { - private static final int HTTP_404 = 404; private final List syncedFolders; private final Context context; - private final int initialFolderNumber; - private final ArrayList remoteFiles; - public ListFileRemoteOperation(List syncedFolders, Context context, int initialFolderNumber) { + private final ArrayList updatedSyncedFoldersId; + + public ListFileRemoteOperation(List syncedFolders, Context context) { Timber.tag(ListFileRemoteOperation.class.getSimpleName()); this.syncedFolders = syncedFolders; this.context = context; - this.initialFolderNumber = initialFolderNumber; - remoteFiles = new ArrayList<>(); + updatedSyncedFoldersId = new ArrayList<>(); } /** @@ -52,140 +52,23 @@ public class ListFileRemoteOperation extends RemoteOperation> run(OwnCloudClient ownCloudClient) { - RemoteOperationResult finalResult; - - boolean atLeastOneDirAsChanged = false; - final ListIterator mSyncedFolderIterator = syncedFolders.listIterator(); - - while (mSyncedFolderIterator.hasNext()) { - try { - Thread.sleep(150); - } catch (InterruptedException exception) { - Timber.v( "listFileRemoteOperation's sleep had been interrupted"); - } - - final SyncedFolder syncedFolder = mSyncedFolderIterator.next(); - if (shouldSkipSyncedFolder(syncedFolder)) { - mSyncedFolderIterator.remove(); - continue; - } + final RemoteFileLister fileLister = new RemoteFileLister(syncedFolders, ownCloudClient); + final boolean isContentToScan = fileLister.listContentToScan(context); - if (syncedFolder.getId() == -1) { - final int syncedFolderId = (int) DbHelper.insertSyncedFolder(syncedFolder, context); - if (syncedFolderId <= 0) { - mSyncedFolderIterator.remove(); - Timber.v("insertion of syncedFolder for %s failed: %s ", syncedFolder.getRemoteFolder(), syncedFolderId); - continue; - } - syncedFolder.setId(syncedFolderId); - } - - final ReadFolderRemoteOperation operation = new ReadFolderRemoteOperation(syncedFolder.getRemoteFolder()); - final RemoteOperationResult result = operation.execute(ownCloudClient); - if (result.isSuccess()) { - final boolean dirHasChanged = onListFileSuccess(mSyncedFolderIterator, syncedFolder, result); - atLeastOneDirAsChanged = dirHasChanged || atLeastOneDirAsChanged; //stay true if set previously - } else { - if (result.getHttpCode() == HTTP_404) { //dir has not been found - atLeastOneDirAsChanged = true; - onHttp404Received(mSyncedFolderIterator, syncedFolder); - } - Timber.d("ReadFolderRemoteOperation failed : http %s, %s => ignored", result.getHttpCode(), result.getLogMessage()); - } - } - finalResult = new RemoteOperationResult<>(RemoteOperationResult.ResultCode.OK); - if (atLeastOneDirAsChanged) { + final RemoteOperationResult result = new RemoteOperationResult<>(RemoteOperationResult.ResultCode.OK); + if (isContentToScan) { DbHelper.updateSyncedFolders(this.syncedFolders, this.context); - finalResult.setResultData(remoteFiles); - } - return finalResult; - } - - - /** - * Analyze list of remote files for a given Directory - * @param iterator ListIterator contains list of syncedFolder to check - * @param syncedFolder the SyncedFolder for the given Directory - * @param result Result of ListRemoteFile operation - * @return true if some directory has changed or its content - */ - private boolean onListFileSuccess(final ListIterator iterator, SyncedFolder syncedFolder, RemoteOperationResult result) { - final int dataSize = result.getData().size(); - final RemoteFile directory = (RemoteFile) result.getData().get(0); - - if (directory.getEtag().equals(syncedFolder.getLastEtag()) - && !DbHelper.syncedFolderHasContentToDownload(syncedFolder.getId(), context)) { - return false; - } - syncedFolder.setLastEtag(directory.getEtag()).setToSync(true); - - final List subFiles = result.getData().subList(1, dataSize); - - for (int i = -1, subFilesSize = subFiles.size(); ++i < subFilesSize;) { - final RemoteFile remoteFile = (RemoteFile) subFiles.get(i); - - if (remoteFile.getMimeType().equals("DIR")) { - final String suffixPath = remoteFile.getRemotePath().substring(syncedFolder.getRemoteFolder().length()); - final SyncedFolder subSyncedFolder = new SyncedFolder(syncedFolder, suffixPath, 0L, ""); //need to set empty etag to allow it to be scan - iterator.add(subSyncedFolder); - iterator.previous(); - } else if (!isHiddenFile(remoteFile)) { - Timber.v("Add remote file %s", remoteFile.getRemotePath()); - this.remoteFiles.add(remoteFile); - } + result.setResultData(fileLister.getContentToScan()); } - return true; - } - - /** - * Handle a case where the checked directory is missing on cloud - * @param iterator ListIterator list of syncedFolder to scan - * @param syncedFolder Missing syncedFolder on cloud - */ - private void onHttp404Received(final ListIterator iterator, SyncedFolder syncedFolder) { - syncedFolder.setToSync(true); + updatedSyncedFoldersId.addAll(fileLister.getSyncedFoldersId()); - final File localFolder = new File(syncedFolder.getLocalFolder()); - final File[] files = localFolder.listFiles(); - if (files != null && files.length == 0) { - localFolder.delete(); - } - - if (!localFolder.exists()) { - if (syncedFolder.getId() > this.initialFolderNumber) { //Do not remove initial syncedFolder - DbHelper.deleteSyncedFolder(syncedFolder.getId(), context); - } - iterator.remove(); - } - } - - - /** - * Indicate if remote file is an hidden file - * @param file Remote file - * @return true if it's an hidden file - */ - private boolean isHiddenFile(RemoteFile file) { - final String fileName = CommonUtils.getFileNameFromPath(file.getRemotePath()); - return fileName != null && fileName.startsWith("."); - } - - /** - * Indicate if we should skip SyncedFolder - * @param syncedFolder - * @return - */ - private boolean shouldSkipSyncedFolder(SyncedFolder syncedFolder) { - final String fileName = CommonUtils.getFileNameFromPath(syncedFolder.getRemoteFolder()); - return (syncedFolder.isMediaType() - && fileName != null && fileName.startsWith(".")) - || !syncedFolder.isScanRemote(); + return result; } /** * @return list of syncedFolder */ - public List getSyncedFolderList(){ - return this.syncedFolders; + public List getSyncedFoldersId(){ + return this.updatedSyncedFoldersId; } } 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 7d6a2239f8e8bcc88645d9cc2984e7dbe6624187..0da565c1a5b804f0f2427ef70583b70d07c52f13 100644 --- a/app/src/main/java/foundation/e/drive/services/ObserverService.java +++ b/app/src/main/java/foundation/e/drive/services/ObserverService.java @@ -41,6 +41,7 @@ import java.util.List; import java.util.ListIterator; import foundation.e.drive.contentScanner.LocalContentScanner; +import foundation.e.drive.contentScanner.LocalFileLister; import foundation.e.drive.contentScanner.RemoteContentScanner; import foundation.e.drive.database.DbHelper; import foundation.e.drive.fileFilters.CrashlogsFileFilter; @@ -69,7 +70,6 @@ public class ObserverService extends Service implements OnRemoteOperationListene private List mSyncedFolders; //List of synced folder private boolean isWorking = false; - private int initialFolderCounter; private Account mAccount; private HashMap syncRequests; //integer is SyncedFileState id; Parcelable is the operation private SynchronizationServiceConnection synchronizationServiceConnection = new SynchronizationServiceConnection(); @@ -113,7 +113,6 @@ public class ObserverService extends Service implements OnRemoteOperationListene } this.syncRequests = new HashMap<>(); - initialFolderCounter = prefs.getInt(AppConstants.INITIALFOLDERS_NUMBER, 0); handlerThread = new HandlerThread("syncService_onResponse"); handlerThread.start(); @@ -263,7 +262,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene } try { - final ListFileRemoteOperation loadOperation = new ListFileRemoteOperation(this.mSyncedFolders, this, this.initialFolderCounter); + final ListFileRemoteOperation loadOperation = new ListFileRemoteOperation(this.mSyncedFolders, this); loadOperation.execute(client, this, handler); } catch (IllegalArgumentException exception) { Timber.e(exception); @@ -288,7 +287,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene } else if (settingsSyncedEnabled) { return DbHelper.getSyncedFolderList(this, false); } else { - return new ArrayList(); + return new ArrayList<>(); } } @@ -309,16 +308,15 @@ public class ObserverService extends Service implements OnRemoteOperationListene final List remoteFiles = ((RemoteOperationResult>)result).getResultData(); if (remoteFiles != null) { - final ListFileRemoteOperation listFileOperation = (ListFileRemoteOperation) operation; - mSyncedFolders = listFileOperation.getSyncedFolderList(); //The list may have been reduced if some directory hasn't changed + final List syncedFoldersId = listFileOperation.getSyncedFoldersId(); - final List syncedFileStateList = DbHelper.getSyncedFileStatesByFolders(this, - getIdsFromFolderToScan()); + final List syncedFileStates = DbHelper.getSyncedFileStatesByFolders(this, + syncedFoldersId); - if (!remoteFiles.isEmpty() || !syncedFileStateList.isEmpty()) { + if (!remoteFiles.isEmpty() || !syncedFileStates.isEmpty()) { final RemoteContentScanner scanner = new RemoteContentScanner(getApplicationContext(), mAccount, mSyncedFolders); - syncRequests.putAll(scanner.scanContent(remoteFiles, syncedFileStateList)); + syncRequests.putAll(scanner.scanContent(remoteFiles, syncedFileStates)); } } @@ -350,22 +348,6 @@ public class ObserverService extends Service implements OnRemoteOperationListene } } - /** - * Method to get Id of SyncedFolder to scan - * @return List id of SyncedFolder to scan - */ - private List getIdsFromFolderToScan() { - final List result = new ArrayList<>(); - for (int i = -1, size = this.mSyncedFolders.size(); ++i < size;) { - final SyncedFolder syncedFolder = this.mSyncedFolders.get(i); - if (syncedFolder.isToSync() ){ - result.add( (long) syncedFolder.getId() ); - } - } - return result; - } - - /* Methods related to device Scanning */ /** @@ -405,76 +387,25 @@ public class ObserverService extends Service implements OnRemoteOperationListene */ private void scanLocalFiles(){ Timber.i("scanLocalFiles()"); - final List fileList = new ArrayList<>(); - final List folderIdList= new ArrayList<>(); - boolean contentToSyncFound = false; - if (CommonUtils.isSettingsSyncEnabled(mAccount)) generateAppListFile(); - final ListIterator iterator = mSyncedFolders.listIterator() ; - - while(iterator.hasNext()) { - final SyncedFolder syncedFolder = iterator.next(); - Timber.v("SyncedFolder : %s, %s, %s, %s, %s", syncedFolder.getLibelle(), syncedFolder.getLocalFolder(), syncedFolder.getLastModified(), syncedFolder.isScanLocal(), syncedFolder.getId()); - - //Check it's not a hidden file - final String fileName = CommonUtils.getFileNameFromPath(syncedFolder.getLocalFolder()); - if (fileName == null || fileName.startsWith(".") || !syncedFolder.isScanLocal()) { - iterator.remove(); - continue; - } - if (syncedFolder.getId() == -1) { - final int syncedFolder_id = (int) DbHelper.insertSyncedFolder(syncedFolder, this); //It will return -1 if there is an error, like an already existing folder with same value - if (syncedFolder_id <= 0) { - iterator.remove(); - continue; - } - syncedFolder.setId(syncedFolder_id); - } - - - final File localDirectory = new File(syncedFolder.getLocalFolder()); //Obtention du fichier local - Timber.v("Local Folder (last modified / exists): %s, %s", localDirectory.lastModified(),localDirectory.exists()); - - if (!localDirectory.exists()) { - contentToSyncFound = true; - folderIdList.add( (long) syncedFolder.getId() ); - continue; - } - - if (localDirectory.lastModified() > syncedFolder.getLastModified() - || DbHelper.syncedFolderHasContentToUpload(syncedFolder.getId(), getApplicationContext())) { - syncedFolder.setLastModified(localDirectory.lastModified()); //@Todo: it would be better to set it after all it's content has been synced - contentToSyncFound = true; - folderIdList.add((long) syncedFolder.getId()); - } + final LocalFileLister fileLister = new LocalFileLister(mSyncedFolders); + final boolean isContentToScan = fileLister.listContentToScan(getApplicationContext()); - final FileFilter filter = FileFilterFactory.getFileFilter( (syncedFolder.isMediaType()) ? "media" : syncedFolder.getLibelle() ); - final File[] subFiles = localDirectory.listFiles(filter); //skip hidden media files - - if (subFiles == null) continue; - for (File subFile : subFiles) { - if (subFile.isDirectory()) { - final SyncedFolder subSyncedFolder = new SyncedFolder(syncedFolder, subFile.getName() + FileUtils.PATH_SEPARATOR, 0L, "");//Need to set lastModified to 0 to handle it on next iteration - iterator.add(subSyncedFolder); - iterator.previous(); - } else if (contentToSyncFound) { - Timber.v("added %s into list of file to scan", subFile.getAbsolutePath()); - fileList.add(subFile); - } - } + if (!isContentToScan) { + return; } - if (contentToSyncFound) { - DbHelper.updateSyncedFolders(mSyncedFolders, this); //@ToDo: maybe do this when all contents will be synced. - final List syncedFileStates = DbHelper.getSyncedFileStatesByFolders(this, - folderIdList); + final List fileList = fileLister.getContentToScan(); + final List folderIdList = fileLister.getSyncedFoldersId(); - if (!syncedFileStates.isEmpty() || !fileList.isEmpty() ) { - final LocalContentScanner scanner= new LocalContentScanner(getApplicationContext(), mAccount, mSyncedFolders); - syncRequests.putAll(scanner.scanContent(fileList, syncedFileStates)); - } + final List syncedFileStates = DbHelper.getSyncedFileStatesByFolders(this, + folderIdList); + + if (!syncedFileStates.isEmpty() || !fileList.isEmpty() ) { + final LocalContentScanner scanner= new LocalContentScanner(getApplicationContext(), mAccount, mSyncedFolders); + syncRequests.putAll(scanner.scanContent(fileList, syncedFileStates)); } } /* end of methods related to device Scanning */ diff --git a/app/src/test/java/foundation/e/drive/TestUtils.java b/app/src/test/java/foundation/e/drive/TestUtils.java index 1da09d5d35710b3560276f3cff40765546aeb66c..19861db817c02ccc0b891b8837efdc16433f2a7d 100644 --- a/app/src/test/java/foundation/e/drive/TestUtils.java +++ b/app/src/test/java/foundation/e/drive/TestUtils.java @@ -155,6 +155,24 @@ public abstract class TestUtils { } + /** + * Delete a local directory with all its content. + * Adapted from https://www.geeksforgeeks.org/java-program-to-delete-a-directory/ + * @param file + */ + public static void deleteDirectory(File file) + { + for (File subfile : file.listFiles()) { + + if (subfile.isDirectory()) { + deleteDirectory(subfile); + } + subfile.delete(); + } + file.delete(); + } + + public static void initializeWorkmanager(Context context) { final Configuration config = new Configuration.Builder() .setMinimumLoggingLevel(Log.DEBUG) diff --git a/app/src/test/java/foundation/e/drive/contentScanner/LocalFileListerTest.java b/app/src/test/java/foundation/e/drive/contentScanner/LocalFileListerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..ee0d7b75692b9c4357da686aef73cefca8d0fa44 --- /dev/null +++ b/app/src/test/java/foundation/e/drive/contentScanner/LocalFileListerTest.java @@ -0,0 +1,373 @@ +/* + * Copyright © ECORP 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 android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.os.Build; + +import androidx.test.core.app.ApplicationProvider; + +import org.junit.After; +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.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import foundation.e.drive.TestUtils; +import foundation.e.drive.database.DbHelper; +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 LocalFileListerTest { + + private static final String TEST_FILE_TREE_ROOT_PATH = "../tmp/"; + + private LocalFileLister fileListerUnderTest; + private final Context context; + private final File testRootDirectory; + + public LocalFileListerTest() { + context = ApplicationProvider.getApplicationContext(); + ShadowLog.stream = System.out; + testRootDirectory = new File(TEST_FILE_TREE_ROOT_PATH); + } + + @Before + public void setUp() { + fileListerUnderTest = new LocalFileLister(null); + } + + + @After + public void tearDown() { + SQLiteDatabase.deleteDatabase(context.getDatabasePath(DbHelper.DATABASE_NAME)); + + if (testRootDirectory.exists()) { + TestUtils.deleteDirectory(testRootDirectory); + } + } + + @Test + public void skipSyncedFolder_notMedia_notHidden_notScannable_shouldReturnTrue() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(false, false, false); + Assert.assertTrue(fileListerUnderTest.skipSyncedFolder(folder)); + } + + @Test + public void skipSyncedFolder_notMedia_notHidden_scannable_shouldReturnFalse() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(false, false, true); + Assert.assertFalse(fileListerUnderTest.skipSyncedFolder(folder)); + } + + @Test + public void skipSyncedFolder_notMedia_hidden_notScannable_shouldReturnTrue() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(false, true, false); + Assert.assertTrue(fileListerUnderTest.skipSyncedFolder(folder)); + } + + @Test + public void skipSyncedFolder_notMedia_hidden_scannable_shouldReturnFalse() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(false, true, true); + Assert.assertFalse(fileListerUnderTest.skipSyncedFolder(folder)); + } + + @Test + public void skipSyncedFolder_media_notHidden_notScannable_shouldReturnTrue() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(true, false, false); + Assert.assertTrue(fileListerUnderTest.skipSyncedFolder(folder)); + } + + @Test + public void skipSyncedFolder_media_notHidden_scannable_shouldReturnFalse() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(true, false, true); + Assert.assertFalse("folder shouldn't be skip but it had", fileListerUnderTest.skipSyncedFolder(folder)); + } + + @Test + public void skipSyncedFolder_media_Hidden_notScannable_shouldReturnTrue() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(true, true, false); + Assert.assertTrue(fileListerUnderTest.skipSyncedFolder(folder)); + } + + @Test + public void skipSyncedFolder_media_Hidden_scannable_shouldReturnTrue() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(true, true, true); + Assert.assertTrue(fileListerUnderTest.skipSyncedFolder(folder)); + } + + private SyncedFolder buildSyncedFolderForSkipSyncedFolderTest(boolean isMedia, boolean isHidden, boolean isLocalScannable) { + final String dirName = isHidden ? ".myFolder/" : "myFolder/"; + return new SyncedFolder("any", TEST_FILE_TREE_ROOT_PATH + dirName, "/remote/path/" + dirName, isLocalScannable, true, true, isMedia); + } + + @Test + public void skipDirectory_lastModifiedSame_noUnsyncedContent_shouldReturnTrue() { + final long lastModified = 123456L; + + final File directory = Mockito.spy(testRootDirectory); + Mockito.when(directory.lastModified()).thenReturn(lastModified); + + final SyncedFolder syncedFolder = new SyncedFolder("any", directory.getAbsolutePath(), "/remote/path/test/", true, true, true, true); + syncedFolder.setId(12); + syncedFolder.setLastModified(lastModified); + + final boolean skip = fileListerUnderTest.skipDirectory(directory, syncedFolder, context); + Assert.assertTrue("directory without new last modified and no content waiting for sync has not been skip", skip); + } + + @Test + public void skipDirectory_lastModifiedSame_unsyncedContent_shouldReturnFalse() { + final long lastModified = 123456L; + + final String dirPath = testRootDirectory.getAbsolutePath(); + + final File directory = Mockito.spy(testRootDirectory); + Mockito.when(directory.lastModified()).thenReturn(lastModified); + + final SyncedFileState dummy = new SyncedFileState(6, "foo.jpg", dirPath + "foo.jpg", "/remote/path/test/foo.jpg", "", 0L, 12, true, 3); + DbHelper.manageSyncedFileStateDB(dummy, "INSERT", context); + + final SyncedFolder syncedFolder = new SyncedFolder("any", dirPath , "/remote/path/test/", true, true, true, true); + syncedFolder.setId(12); + syncedFolder.setLastModified(lastModified); + + final boolean skip = fileListerUnderTest.skipDirectory(directory, syncedFolder, context); + Assert.assertFalse("directory without new last modified but content waiting for sync has been skip", skip); + } + + @Test + public void skipDirectory_lastModifiedChanged_noUnsyncedContent_shouldReturnFalse() { + final String dirPath = testRootDirectory.getAbsolutePath(); + + final File directory = Mockito.spy(testRootDirectory); + Mockito.when(directory.lastModified()).thenReturn(654321L); + + final SyncedFolder syncedFolder = new SyncedFolder("any", dirPath, "/remote/path/test/", true, true, true, true); + syncedFolder.setId(12); + syncedFolder.setLastModified(123456L); + + final boolean skip = fileListerUnderTest.skipDirectory(directory, syncedFolder, context); + Assert.assertFalse("directory with new last modified and no content waiting for sync has been skip", skip); + } + + @Test + public void skipDirectory_lastModifiedChanged_unsyncedContent_shouldReturnFalse() { + final String dirPath = testRootDirectory.getAbsolutePath(); + + final File directory = Mockito.spy(testRootDirectory); + Mockito.when(directory.lastModified()).thenReturn(654321L); + + final SyncedFolder syncedFolder = new SyncedFolder("any", dirPath, "/remote/path/test/", true, true, true, true); + syncedFolder.setId(12); + syncedFolder.setLastModified(123456L); + + final SyncedFileState dummy = new SyncedFileState(6, "foo.jpg", dirPath + "foo.jpg", "/remote/path/test/foo.jpg", "", 0L, 12, true, 3); + DbHelper.manageSyncedFileStateDB(dummy, "INSERT", context); + + final boolean skip = fileListerUnderTest.skipDirectory(directory, syncedFolder, context); + Assert.assertFalse("directory with new last modified and content waiting for sync has been skip", skip); + } + + //Test about inner class: DirectoryLoader + @Test + public void loadDirectory_dirNotExist_shouldReturnTrue() { + final String dirPath = testRootDirectory.getAbsolutePath(); + + final SyncedFolder folder = new SyncedFolder("any", dirPath, "/remote/path/test", true); + + final LocalFileLister.LocalFolderLoader folderLoader = fileListerUnderTest.createFolderLoader(); + + final boolean loadSuccess = folderLoader.load(folder); + Assert.assertTrue("Loading remote folder resulting in 404 should return true", loadSuccess); + Assert.assertTrue(folderLoader.getFolderWrapper().isMissing()); + } + + @Test + public void loadDirectory_dirExist_noContent_shouldReturnTrue() { + final String dirPath = testRootDirectory.getAbsolutePath(); + + final SyncedFolder folder = new SyncedFolder("any", dirPath, "/remote/path/test/", true); + + final boolean dirCreationSucceed = testRootDirectory.mkdirs(); + + Assert.assertTrue("Cannot create test folder: " + dirPath, dirCreationSucceed); + + final LocalFileLister.LocalFolderLoader folderLoader = fileListerUnderTest.createFolderLoader(); + + final boolean loadSuccess = folderLoader.load(folder); + final FolderWrapper wrapper = folderLoader.getFolderWrapper(); + Assert.assertTrue("Loading remote folder resulting in 404 should return true", loadSuccess); + Assert.assertFalse("dir shouldExist!", wrapper.isMissing()); + Assert.assertTrue("List of folderWrapper's content expected to be empty but contains: " + wrapper.getContent().size(), wrapper.getContent().isEmpty()); + } + + @Test + public void loadDirectory_dirExist_withSubContent_shouldReturnTrue() { + final String dirPath = testRootDirectory.getAbsolutePath(); + + final SyncedFolder folder = new SyncedFolder("any", dirPath, "/remote/path/test/", true); + final boolean dirCreationSucceed = testRootDirectory.mkdirs(); + Assert.assertTrue("cannot create test folder: " + dirPath, dirCreationSucceed); + + + final File subContent = new File(testRootDirectory, "foo.txt"); + try { + Assert.assertTrue("Can't create subFile", subContent.createNewFile()); + } catch (IOException exception) { + exception.printStackTrace(); + Assert.fail("IO Exception: Can't create new file as content of test folder"); + } + + final LocalFileLister.LocalFolderLoader folderLoader = fileListerUnderTest.createFolderLoader(); + + final boolean loadSuccess = folderLoader.load(folder); + final FolderWrapper wrapper = folderLoader.getFolderWrapper(); + Assert.assertTrue("Loading remote folder resulting in 404 should return true", loadSuccess); + Assert.assertFalse("dir shouldExist!", wrapper.isMissing()); + Assert.assertEquals("List of folderWrapper's content size expected to one but was: " + wrapper.getContent().size(), 1, wrapper.getContent().size()); + } + + + /** + * Run on a more realistic files tree to check the whole behaviour with more than + * one directory: + * + * It should contains : + * - one directory with a subdirectory with some files in both + * - one hidden directory with some files + * - one empty directory + * - one "new" directory with subfiles + * + * with correspond syncedFolder in database: + * - except for one, so it will considered as new + * Something like: + * + * ## File tree + * >Folder 1 (unChanged) + * --> .File 1 (should be ignored) + * --> File 2 (should be ignored) + * --> Folder 2 (unsynced dir) + * ----> File 3 (new file should be added) + * ----> .File 4 (should be ignored) + * >Folder 3 (unChanged) + * -->Folder4 (new dir (so unsynced) + * ----> File 5 (new file should be added) + * -->.Folder5 (skip: hidden dir) + * ----> File 6 (skip because directory is hidden) + * + * + * ## SyncedFolder + * SyncedFolder 1 + * SyncedFolder 2 with empty lastModified + * SyncedFolder 3 + * SyncedFolderRemoved which correspond to a missing directory + */ + @Test + public void test_listContentToScan() { + Assert.assertTrue("Preparation, of file Tree and syncedFolder has failed", prepareFakeContent()); + Assert.assertTrue("File listing failed", fileListerUnderTest.listContentToScan(context)); + + final List foldersId = fileListerUnderTest.getSyncedFoldersId(); + Assert.assertEquals("List of folders to scan's id should contains 3 but contained: " + foldersId.size(), 3, foldersId.size()); + Assert.assertTrue(foldersId.contains(2L)); //id of changed folder detected + Assert.assertTrue(foldersId.contains(5L)); //id of new folder detected + Assert.assertTrue(foldersId.contains(4L)); //id of Removed Folder detected + final List contentListed = fileListerUnderTest.getContentToScan(); + Assert.assertEquals("ContentListed should have 2 files but contained: " + contentListed.size(), 2, contentListed.size()); + } + + private boolean prepareFakeContent() { + /* + * Generate folders + */ + if (!testRootDirectory.mkdirs()) return false; + + final File folder1 = new File(testRootDirectory, "folder1"); + if (!folder1.mkdirs()) return false; + final File folder2 = new File(folder1, "folder2"); + if (!folder2.mkdirs()) return false; + + final File folder3 = new File(testRootDirectory, "folder3"); + if (!folder3.mkdirs()) return false; + final File folder4 = new File(folder3, "folder4"); + if (!folder4.mkdirs()) return false; + final File folder5 = new File(folder3, ".folder5"); + if (!folder5.mkdirs()) return false; + + + /* + * Generate files + * /!\ must be done before syncedFolders generation + * otherwise lastModified value of directory will change due to file creation + */ + try { + final File file1 = new File(folder1, ".file1.txt"); + if (!file1.createNewFile()) return false; + + final File file2 = new File(folder1, "file2.txt"); + if (!file2.createNewFile()) return false; + + final File file3 = new File(folder2, "file3.txt"); + if (!file3.createNewFile()) return false; + + final File file4 = new File(folder2, ".file4.txt"); + if (!file4.createNewFile()) return false; + + final File file5 = new File(folder4, "file5.txt"); + if (!file5.createNewFile()) return false; + + final File file6 = new File(folder5, "file6.txt"); + if (!file6.createNewFile()) return false; + } catch (IOException exception) { + exception.printStackTrace(); + return false; + } + + /* + * Generate SyncedFolders + */ + final SyncedFolder syncedFolder1 = new SyncedFolder("any", folder1.getAbsolutePath() + "/", "/remote/path/myFolder1/", true); + syncedFolder1.setLastModified(folder1.lastModified()); + syncedFolder1.setId((int) DbHelper.insertSyncedFolder(syncedFolder1, context)); + final SyncedFolder syncedFolder2 = new SyncedFolder("any", folder2.getAbsolutePath() + "/", "/remote/path/myFolder2/", true); + syncedFolder2.setLastModified(0L); + syncedFolder2.setId((int) DbHelper.insertSyncedFolder(syncedFolder2, context)); + final SyncedFolder syncedFolder3 = new SyncedFolder("any", folder3.getAbsolutePath() + "/", "/remote/path/myFolder3/", true); + syncedFolder3.setLastModified(folder3.lastModified()); + syncedFolder3.setId((int) DbHelper.insertSyncedFolder(syncedFolder3, context)); + final SyncedFolder syncedFolderRemoved = new SyncedFolder("any", testRootDirectory.getAbsolutePath() + "/lambda/", "/remote/lambda", true); + syncedFolderRemoved.setLastModified(987654321L); + syncedFolderRemoved.setLastEtag("123456789"); + syncedFolderRemoved.setId((int) DbHelper.insertSyncedFolder(syncedFolderRemoved, context)); + + final ArrayList syncedFolders = new ArrayList<>(); + syncedFolders.add(syncedFolder1); + syncedFolders.add(syncedFolder2); + syncedFolders.add(syncedFolder3); + syncedFolders.add(syncedFolderRemoved); + fileListerUnderTest = new LocalFileLister(syncedFolders); + + return true; + } +} diff --git a/app/src/test/java/foundation/e/drive/contentScanner/RemoteFileListerTest.java b/app/src/test/java/foundation/e/drive/contentScanner/RemoteFileListerTest.java new file mode 100644 index 0000000000000000000000000000000000000000..5e3e2431f60024b808e207585e0b1732a74d3ad5 --- /dev/null +++ b/app/src/test/java/foundation/e/drive/contentScanner/RemoteFileListerTest.java @@ -0,0 +1,518 @@ +/* + * Copyright © ECORP 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.accounts.Account; +import android.accounts.AccountManager; +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.os.Build; + +import androidx.test.core.app.ApplicationProvider; + +import com.owncloud.android.lib.common.OwnCloudClient; +import com.owncloud.android.lib.common.operations.RemoteOperationResult; +import com.owncloud.android.lib.resources.files.model.RemoteFile; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Ignore; +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.List; + +import foundation.e.drive.TestUtils; +import foundation.e.drive.database.DbHelper; +import foundation.e.drive.models.SyncedFileState; +import foundation.e.drive.models.SyncedFolder; +import foundation.e.drive.utils.DavClientProvider; + +/** + * @author vincent Bourgmayer + */ +@RunWith(RobolectricTestRunner.class) +@Config(sdk = Build.VERSION_CODES.O, manifest = Config.NONE) +public class RemoteFileListerTest { + private static final String DIR_NO_SKIP_EXPECTED = "directory shouldn't have been skipped"; + private static final String DIR_SKIP_EXPECTED = "directory should have been skipped"; + private static final String TEST_FILE_TREE_ROOT_PATH = "/remote/"; + + private RemoteFileLister fileListerUnderTest; + private final Account account; + private final OwnCloudClient ocClient; + private final Context context; + + public RemoteFileListerTest() { + context = ApplicationProvider.getApplicationContext(); + ShadowLog.stream = System.out; + TestUtils.loadServerCredentials(); + TestUtils.prepareValidAccount(AccountManager.get(context)); + account = TestUtils.getValidAccount(); + ocClient = DavClientProvider.getInstance().getClientInstance(account, context); + } + + @Before + public void setUp() { + fileListerUnderTest = new RemoteFileLister(null, ocClient); + SQLiteDatabase.deleteDatabase(context.getDatabasePath(DbHelper.DATABASE_NAME)); + } + + @Test + public void skipSyncedFolder_notMedia_notHidden_notScannable_shouldReturnTrue() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(false, false, false); + final boolean skipFolder = fileListerUnderTest.skipSyncedFolder(folder); + Assert.assertTrue("Not media, not hidden, not remotly scannable " + DIR_SKIP_EXPECTED, skipFolder); + } + + @Test + public void skipSyncedFolder_notMedia_notHidden_shouldReturnFalse() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(false, false, true); + final boolean skipFolder = fileListerUnderTest.skipSyncedFolder(folder); + Assert.assertFalse("Not media, not hidden, remotly scannable " + DIR_NO_SKIP_EXPECTED, skipFolder); + } + + @Test + public void skipSyncedFolder_notMedia_hidden_notScannable_shouldReturnTrue() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(false, true, false); + final boolean skipFolder = fileListerUnderTest.skipSyncedFolder(folder); + Assert.assertTrue("Not media, hidden, not remotly scannable " + DIR_SKIP_EXPECTED, skipFolder); + } + + @Test + public void skipSyncedFolder_notMedia_hidden_scannable_shouldReturnFalse() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(false, true, true); + final boolean skipFolder = fileListerUnderTest.skipSyncedFolder(folder); + Assert.assertFalse("Not media, hidden, remotly scannable " + DIR_NO_SKIP_EXPECTED, skipFolder); + } + + @Test + public void skipSyncedFolder_media_notHidden_notScannable_shouldReturnTrue() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(true, false, false); + final boolean skipFolder = fileListerUnderTest.skipSyncedFolder(folder); + Assert.assertTrue("media, not hidden, not remotly scannable " + DIR_SKIP_EXPECTED, skipFolder); + } + + @Test + public void skipSyncedFolder_media_notHidden_scannable_shouldReturnFalse() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(true, false, true); + final boolean skipFolder = fileListerUnderTest.skipSyncedFolder(folder); + Assert.assertFalse("media, not hidden, remotly scannable " + DIR_NO_SKIP_EXPECTED, skipFolder); + } + + @Test + public void skipSyncedFolder_media_hidden_notScannable_shouldReturnTrue() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(true, true, false); + final boolean skipFolder = fileListerUnderTest.skipSyncedFolder(folder); + Assert.assertTrue("media, hidden, not remotly scannable " + DIR_SKIP_EXPECTED, skipFolder); + } + + @Test + public void skipSyncedFolder_media_hidden_scannable_shouldReturnTrue() { + final SyncedFolder folder = buildSyncedFolderForSkipSyncedFolderTest(true, true, true); + final boolean skipFolder = fileListerUnderTest.skipSyncedFolder(folder); + Assert.assertTrue("media, hidden, remotly scannable " + DIR_SKIP_EXPECTED, skipFolder); + } + + private SyncedFolder buildSyncedFolderForSkipSyncedFolderTest(boolean isMedia, boolean isHidden, boolean isRemoteScannable) { + final String dirName = isHidden ? ".myFolder/" : "myFolder/"; + return new SyncedFolder("any", "/local/path/" + dirName, TEST_FILE_TREE_ROOT_PATH + dirName, true, isRemoteScannable, true, isMedia); + } + + + @Test + public void skipFile_hidden_shouldReturnTrue() { + final String hiddenRemoteFilePath = TEST_FILE_TREE_ROOT_PATH + ".foo.png"; + final RemoteFile file = Mockito.mock(RemoteFile.class); + when(file.getRemotePath()).thenReturn(hiddenRemoteFilePath); + + final boolean skipFile = fileListerUnderTest.skipFile(file); + Assert.assertTrue("Hidden remoteFile (" + hiddenRemoteFilePath + ") should have been skipped", skipFile); + } + + @Test + public void skipFile_notHidden_shouldReturnFalse() { + final String notHiddenRemoteFilePath = TEST_FILE_TREE_ROOT_PATH + "foo.png"; + final RemoteFile file = Mockito.mock(RemoteFile.class); + when(file.getRemotePath()).thenReturn(notHiddenRemoteFilePath); + + final boolean skipFile = fileListerUnderTest.skipFile(file); + Assert.assertFalse("Not hidden remoteFile (" + notHiddenRemoteFilePath + ") should not have been skipped", skipFile); + } + + @Test + public void skipFile_emptyPath_shouldReturnTrue() { + final String emptyPath = ""; + final RemoteFile file = Mockito.mock(RemoteFile.class); + when(file.getRemotePath()).thenReturn(emptyPath); + + final boolean skipFile = fileListerUnderTest.skipFile(file); + Assert.assertTrue("RemoteFile with empty path (" + emptyPath + ") should have been skipped", skipFile); + } + + @Test + @Ignore + public void skipFile_invalidPath_shouldReturnTrue() { + final String invalidPath = "/invalid/path/"; + /*in this case: No file, just a directory + the current implementation of CommonUtils.getFileName(String path) + will return "path" while empty string should be expected. + todo: fix CommonUtils.getFileName(String path) */ + final RemoteFile file = Mockito.mock(RemoteFile.class); + when(file.getRemotePath()).thenReturn(invalidPath); + + final boolean skipFile = fileListerUnderTest.skipFile(file); + Assert.assertTrue("RemoteFile with empty path (" + invalidPath + ") should have been skipped", skipFile); + } + + @Test + public void skipDirectory_etagChanged_noUnsyncedContent_shouldReturnFalse() { + final RemoteFile remoteFile = Mockito.mock(RemoteFile.class); + when(remoteFile.getEtag()).thenReturn("6666bbbb"); + + final SyncedFolder syncedFolder = new SyncedFolder("", "local", TEST_FILE_TREE_ROOT_PATH, true); + syncedFolder.setId(12); + syncedFolder.setLastEtag("5555aaaa"); + + final boolean skip = fileListerUnderTest.skipDirectory(remoteFile, syncedFolder, context); + + Assert.assertFalse("Remote directory with new etag should not have been skipped but it had", skip); + } + + @Test + public void skipDirectory_etagSame_noUnsyncedContent_shouldReturnTrue() { + final RemoteFile remoteFile = Mockito.mock(RemoteFile.class); + when(remoteFile.getEtag()).thenReturn("5555aaaa"); + + final SyncedFolder syncedFolder = new SyncedFolder("", "local", TEST_FILE_TREE_ROOT_PATH, true); + syncedFolder.setId(12); + syncedFolder.setLastEtag("5555aaaa"); + + final boolean skip = fileListerUnderTest.skipDirectory(remoteFile, syncedFolder, context); + + Assert.assertTrue("Remote directory with no new etag should have been skipped but had not", skip); + } + + @Test + public void skipDirectory_etagSame_contentToSync_shouldReturnFalse() { + final RemoteFile remoteFile = Mockito.mock(RemoteFile.class); + when(remoteFile.getEtag()).thenReturn("5555aaaa"); + + final SyncedFolder syncedFolder = new SyncedFolder("", "local", TEST_FILE_TREE_ROOT_PATH, true); + syncedFolder.setId(12); + syncedFolder.setLastEtag("5555aaaa"); + + final SyncedFileState dummy = new SyncedFileState(6, "foo.jpg", "local/foo.jpg", TEST_FILE_TREE_ROOT_PATH + "foo.jpg", "7777", 0L, 12, true, 3); + DbHelper.manageSyncedFileStateDB(dummy, "INSERT", context); + + final boolean skip = fileListerUnderTest.skipDirectory(remoteFile, syncedFolder, context); + Assert.assertFalse("Remote directory with no new etag but there is content to sync from DB: should not have been skipped but it had", skip); + } + + @Test + public void skipDirectory_etagChanged_contentToSync_shouldReturnFalse() { + final RemoteFile remoteFile = Mockito.mock(RemoteFile.class); + when(remoteFile.getEtag()).thenReturn("6666bbbb"); + + final SyncedFolder syncedFolder = new SyncedFolder("", "local", TEST_FILE_TREE_ROOT_PATH, true); + syncedFolder.setId(12); + syncedFolder.setLastEtag("5555aaaa"); + + final SyncedFileState dummy = new SyncedFileState(6, "foo.jpg", "local/foo.jpg", TEST_FILE_TREE_ROOT_PATH + "foo.jpg", "7777", 0L, 12, true, 3); + DbHelper.manageSyncedFileStateDB(dummy, "INSERT", context); + + final boolean skip = fileListerUnderTest.skipDirectory(remoteFile, syncedFolder, context); + Assert.assertFalse("Remote directory with new etag and there is content to sync from DB: should not have been skipped but it had", skip); + } + + //Test about inner class: DirectoryLoader + @Test + public void loadDirectory_serverResponse404_shouldReturnTrue() { + final String remotePath = TEST_FILE_TREE_ROOT_PATH + "myFolder/"; + final SyncedFolder folder = new SyncedFolder("any", "local/myFolder", remotePath, true); + + final RemoteOperationResult fakeResult = Mockito.mock(RemoteOperationResult.class); + when(fakeResult.getHttpCode()).thenReturn(404); + when(fakeResult.isSuccess()).thenReturn(false); + + final RemoteFileLister.RemoteFolderLoader mockLoaderSpied = Mockito.spy(fileListerUnderTest.createFolderLoader()); + when(mockLoaderSpied.readRemoteFolder(remotePath, ocClient)).thenReturn(fakeResult); + + final boolean loadSuccess = mockLoaderSpied.load(folder); + Assert.assertTrue("Loading remote folder resulting in 404 should return true", loadSuccess); + } + + @Test + public void loadDirectory_serverResponse405_shouldReturnFalse() { + final String remotePath = TEST_FILE_TREE_ROOT_PATH + "myFolder/"; + final SyncedFolder folder = new SyncedFolder("any", "local/myFolder", remotePath, true); + + final RemoteOperationResult fakeResult = Mockito.mock(RemoteOperationResult.class); + when(fakeResult.getHttpCode()).thenReturn(405); + when(fakeResult.isSuccess()).thenReturn(false); + + final RemoteFileLister.RemoteFolderLoader mockLoaderSpied = Mockito.spy(fileListerUnderTest.createFolderLoader()); + when(mockLoaderSpied.readRemoteFolder(remotePath, ocClient)).thenReturn(fakeResult); + + final boolean loadSuccess = mockLoaderSpied.load(folder); + Assert.assertFalse("Loading remote folder resulting in 405 should return false", loadSuccess); + } + + @Test + public void loadDirectory_successAndErrorCode_shouldReturnTrue() { + final String remotePath = TEST_FILE_TREE_ROOT_PATH + "myFolder/"; + final SyncedFolder folder = new SyncedFolder("any", "local/myFolder", remotePath, true); + + final RemoteOperationResult fakeResult = Mockito.mock(RemoteOperationResult.class); + when(fakeResult.getHttpCode()).thenReturn(405); + when(fakeResult.isSuccess()).thenReturn(true); + + final ArrayList fakeData = new ArrayList<>(); + final RemoteFile mockRemoteFile = Mockito.mock(RemoteFile.class); + fakeData.add(mockRemoteFile); + when(fakeResult.getData()).thenReturn(fakeData); + + final RemoteFileLister.RemoteFolderLoader spiedLoaderUnderTest = Mockito.spy(fileListerUnderTest.createFolderLoader()); + when(spiedLoaderUnderTest.readRemoteFolder(remotePath, ocClient)).thenReturn(fakeResult); + + final boolean loadSuccess = spiedLoaderUnderTest.load(folder); + Assert.assertTrue("Loading remote folder with success despite error code (405) should return true", loadSuccess); + Assert.assertEquals("Loaded Folder is not the one expected despite loading success", mockRemoteFile, spiedLoaderUnderTest.getFolderWrapper().getFolder()); + } + + @Test + public void loadDirectory_successAndRemoteContent_shouldReturnTrue() { + final String remotePath = TEST_FILE_TREE_ROOT_PATH + "myFolder/"; + final SyncedFolder folder = new SyncedFolder("any", "local/myFolder", remotePath, true); + + final RemoteOperationResult fakeResult = Mockito.mock(RemoteOperationResult.class); + when(fakeResult.getHttpCode()).thenReturn(405); + when(fakeResult.isSuccess()).thenReturn(true); + + final ArrayList fakeData = new ArrayList<>(); + final RemoteFile mockDirRemoteFile = Mockito.mock(RemoteFile.class); + fakeData.add(mockDirRemoteFile); + final RemoteFile mockContentRemoteFile1 = Mockito.mock(RemoteFile.class); + final RemoteFile mockContentRemoteFile2 = Mockito.mock(RemoteFile.class); + fakeData.add(mockContentRemoteFile1); + fakeData.add(mockContentRemoteFile2); + when(fakeResult.getData()).thenReturn(fakeData); + + final RemoteFileLister.RemoteFolderLoader spiedLoaderUnderTest = Mockito.spy(fileListerUnderTest.createFolderLoader()); + when(spiedLoaderUnderTest.readRemoteFolder(remotePath, ocClient)).thenReturn(fakeResult); + + final boolean loadSuccess = spiedLoaderUnderTest.load(folder); + Assert.assertTrue("Loading remote folder with success despite error code (405) should return true", loadSuccess); + Assert.assertEquals("Loaded Folder is not the one expected despite loading success", mockDirRemoteFile, spiedLoaderUnderTest.getFolderWrapper().getFolder()); + Assert.assertEquals("mockDirRemoteFile should have been the directory loader but is not", 2, spiedLoaderUnderTest.getFolderWrapper().getContent().size()); + Assert.assertTrue("mockContentRemoteFile1 should have been in the result of loading but is not", spiedLoaderUnderTest.getFolderWrapper().getContent().contains(mockContentRemoteFile1)); + Assert.assertTrue("mockContentRemoteFile2 should have been in the result of loading but is not", spiedLoaderUnderTest.getFolderWrapper().getContent().contains(mockContentRemoteFile2)); + } + + @Test + public void loadDirectory_successButNoData_shouldReturnFalse() { + final String remotePath = TEST_FILE_TREE_ROOT_PATH + "myFolder/"; + final SyncedFolder folder = new SyncedFolder("any", "local/myFolder", remotePath, true); + + final RemoteOperationResult fakeResult = Mockito.mock(RemoteOperationResult.class); + when(fakeResult.getHttpCode()).thenReturn(204); + when(fakeResult.isSuccess()).thenReturn(true); + + final ArrayList fakeData = new ArrayList<>(); + when(fakeResult.getData()).thenReturn(fakeData); + + final RemoteFileLister.RemoteFolderLoader mockLoaderSpied = Mockito.spy(fileListerUnderTest.createFolderLoader()); + when(mockLoaderSpied.readRemoteFolder(remotePath, ocClient)).thenReturn(fakeResult); + + final boolean loadSuccess = mockLoaderSpied.load(folder); + Assert.assertFalse("Loading remote folder resulting in 204 but without any RemoteFile should return false", loadSuccess); + } + + + /** + * Test the whole behaviour of localFileLister with a fake file's tree + * + * ## File tree + * >Folder 1 (unChanged) + * --> File 1 (should be ignored) + * --> Folder 2 (unsynced dir) + * ----> File 2 (new file should be added) + * ----> .File 3 (should be ignored) + * >Folder 3 (unChanged) + * -->Folder4 (skip because folder 3 unchanged) + * ----> File 4 (skip because folder 3 unchanged) + * -->.Folder5 (skip: hidden dir) + * ----> File 5 (skip because directory is hidden) + * + * There is also the missing remote folder, which is expected to be in the result's folder's id + */ + @Test + public void test_listContentToScan() { + prepareFakeContent(); + Assert.assertTrue("File listing failed", fileListerUnderTest.listContentToScan(context)); + + final List foldersId = fileListerUnderTest.getSyncedFoldersId(); + Assert.assertEquals("List of folders to scan's id should contains 2 but contained: " + foldersId.size(), 2, foldersId.size()); + + final List contentListed = fileListerUnderTest.getContentToScan(); + Assert.assertEquals("ContentListed should have 1 files but contained: " + contentListed.size(), 1, contentListed.size()); + } + + /** + * Generate fake file tree & mock object for a full test of RemoteFileLister + */ + private void prepareFakeContent() { + /* + * Generate folders + */ + final String folder1Path = TEST_FILE_TREE_ROOT_PATH + "folder1/"; + final RemoteFile folder1 = Mockito.mock(RemoteFile.class); //unchanged + when(folder1.getRemotePath()).thenReturn(folder1Path); + when(folder1.getEtag()).thenReturn("123456789"); + when(folder1.getMimeType()).thenReturn("DIR"); + + final String folder2Path = folder1Path + "folder2/"; + final RemoteFile folder2 = Mockito.mock(RemoteFile.class); //updated one + when(folder2.getRemotePath()).thenReturn(folder2Path); + when(folder2.getEtag()).thenReturn("234567891"); + when(folder2.getMimeType()).thenReturn("DIR"); + + final String folder3Path = TEST_FILE_TREE_ROOT_PATH + "folder3/"; + final RemoteFile folder3 = Mockito.mock(RemoteFile.class); //unchanged + when(folder3.getRemotePath()).thenReturn(folder3Path); + when(folder3.getEtag()).thenReturn("345678912"); + when(folder3.getMimeType()).thenReturn("DIR"); + + final String folder4Path = folder3Path + "folder4/"; + final RemoteFile folder4 = Mockito.mock(RemoteFile.class); //new one + when(folder4.getRemotePath()).thenReturn(folder4Path); + when(folder4.getEtag()).thenReturn("456789123"); + when(folder4.getMimeType()).thenReturn("DIR"); + + final String folder5Path = folder3.getRemotePath() + ".folder5/"; + final RemoteFile folder5 = Mockito.mock(RemoteFile.class); //hidden one + when(folder5.getRemotePath()).thenReturn(folder5Path); + when(folder5.getEtag()).thenReturn("567891234"); + when(folder5.getMimeType()).thenReturn("DIR"); + + /* + * Generate files + */ + final RemoteFile file1 = Mockito.mock(RemoteFile.class); //ignored because dir unchanged + when(file1.getRemotePath()).thenReturn(folder1Path + "file1.png"); + when(file1.getMimeType()).thenReturn("any"); + final RemoteFile file2 = Mockito.mock(RemoteFile.class); //new file one, should be added + when(file2.getRemotePath()).thenReturn(folder2Path + "file2.png"); + when(file2.getMimeType()).thenReturn("any"); + final RemoteFile file3 = Mockito.mock(RemoteFile.class); //hidden one: ignored + when(file3.getRemotePath()).thenReturn(folder2Path + ".file3.png"); + when(file3.getMimeType()).thenReturn("any"); + final RemoteFile file4 = Mockito.mock(RemoteFile.class); //new one should be added + when(file4.getRemotePath()).thenReturn(folder4Path + "file4.png"); + when(file4.getMimeType()).thenReturn("any"); + final RemoteFile file5 = Mockito.mock(RemoteFile.class); //ignored because dir is hidden + when(file5.getRemotePath()).thenReturn(folder5Path + "file5.png"); + when(file5.getMimeType()).thenReturn("any"); + + /* + * Generate SyncedFolders + */ + final SyncedFolder syncedFolder1 = new SyncedFolder("any", "local/folder1/", folder1Path, true); + syncedFolder1.setLastEtag(folder1.getEtag()); + syncedFolder1.setId((int) DbHelper.insertSyncedFolder(syncedFolder1, context)); + + final SyncedFolder syncedFolder2 = new SyncedFolder("any", "local/folder1/folder2/", folder2Path, true); + syncedFolder2.setLastEtag(""); + syncedFolder2.setId((int) DbHelper.insertSyncedFolder(syncedFolder2, context)); + + final SyncedFolder syncedFolder3 = new SyncedFolder("any","local/folder3/", folder3Path, true); + syncedFolder3.setLastEtag(folder3.getEtag()); + syncedFolder3.setId((int) DbHelper.insertSyncedFolder(syncedFolder3, context)); + + final SyncedFolder syncedFolderRemoved = new SyncedFolder("any", "local/missingFolder/", TEST_FILE_TREE_ROOT_PATH + "lambda/", true); + syncedFolderRemoved.setLastModified(987654321L); + syncedFolderRemoved.setLastEtag("123456789"); + syncedFolderRemoved.setId((int) DbHelper.insertSyncedFolder(syncedFolderRemoved, context)); + + final ArrayList syncedFolders = new ArrayList<>(); + syncedFolders.add(syncedFolder1); + syncedFolders.add(syncedFolder2); + syncedFolders.add(syncedFolder3); + syncedFolders.add(syncedFolderRemoved); + + fileListerUnderTest = Mockito.spy(new RemoteFileLister(syncedFolders, ocClient)); + + + /* Mock the method depending to network and cloud */ + final RemoteFileLister.RemoteFolderLoader spiedDirLoader = Mockito.spy(fileListerUnderTest.createFolderLoader()); + + final RemoteOperationResult folder1Result = Mockito.mock(RemoteOperationResult.class); + when(folder1Result.getHttpCode()).thenReturn(207); + when(folder1Result.isSuccess()).thenReturn(true); + final ArrayList folder1Data = new ArrayList<>(); + folder1Data.add(folder1); + folder1Data.add(file1); + folder1Data.add(folder2); + when(folder1Result.getData()).thenReturn(folder1Data); + when(spiedDirLoader.readRemoteFolder(syncedFolder1.getRemoteFolder(), ocClient)).thenReturn(folder1Result); + + final RemoteOperationResult folder2Result = Mockito.mock(RemoteOperationResult.class); + when(folder2Result.getHttpCode()).thenReturn(207); + when(folder2Result.isSuccess()).thenReturn(true); + final ArrayList folder2Data = new ArrayList<>(); + folder2Data.add(folder2); + folder2Data.add(file2); + folder2Data.add(file3); + when(folder2Result.getData()).thenReturn(folder2Data); + when(spiedDirLoader.readRemoteFolder(syncedFolder2.getRemoteFolder(), ocClient)).thenReturn(folder2Result); + + final RemoteOperationResult folder3Result = Mockito.mock(RemoteOperationResult.class); + when(folder3Result.getHttpCode()).thenReturn(207); + when(folder3Result.isSuccess()).thenReturn(true); + final ArrayList folder3Data = new ArrayList<>(); + folder3Data.add(folder3); + folder3Data.add(folder4); + folder3Data.add(folder5); + when(folder3Result.getData()).thenReturn(folder3Data); + when(spiedDirLoader.readRemoteFolder(syncedFolder3.getRemoteFolder(), ocClient)).thenReturn(folder3Result); + + + final RemoteOperationResult folder4Result = Mockito.mock(RemoteOperationResult.class); + when(folder4Result.getHttpCode()).thenReturn(207); + when(folder4Result.isSuccess()).thenReturn(true); + final ArrayList folder4Data = new ArrayList<>(); + folder4Data.add(folder4); + folder4Data.add(file4); + when(folder4Result.getData()).thenReturn(folder4Data); + when(spiedDirLoader.readRemoteFolder(folder4.getRemotePath(), ocClient)).thenReturn(folder4Result); + + final RemoteOperationResult folder5Result = Mockito.mock(RemoteOperationResult.class); + when(folder5Result.getHttpCode()).thenReturn(207); + when(folder5Result.isSuccess()).thenReturn(true); + final ArrayList folder5Data = new ArrayList<>(); + folder5Data.add(folder4); + folder5Data.add(file4); + when(folder5Result.getData()).thenReturn(folder5Data); + when(spiedDirLoader.readRemoteFolder(folder5.getRemotePath(), ocClient)).thenReturn(folder5Result); + + final RemoteOperationResult missingFolderResult = Mockito.mock(RemoteOperationResult.class); + when(missingFolderResult.getHttpCode()).thenReturn(404); + when(missingFolderResult.isSuccess()).thenReturn(false); + when(spiedDirLoader.readRemoteFolder(syncedFolderRemoved.getRemoteFolder(), ocClient)).thenReturn(missingFolderResult); + + when(fileListerUnderTest.createFolderLoader()).thenReturn(spiedDirLoader); + } +}