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 47661901f48c89ab30d04f8b3f932e65d92794f8..cd8416189e74f94884a366fe3020e9dcdddd3ec8 100644 --- a/app/src/main/java/foundation/e/drive/contentScanner/AbstractContentScanner.java +++ b/app/src/main/java/foundation/e/drive/contentScanner/AbstractContentScanner.java @@ -10,27 +10,106 @@ package foundation.e.drive.contentScanner; import android.accounts.Account; import android.content.Context; +import com.owncloud.android.lib.resources.files.FileUtils; + import java.util.HashMap; +import java.util.List; +import java.util.ListIterator; import foundation.e.drive.models.SyncRequest; +import foundation.e.drive.models.SyncedFileState; +import foundation.e.drive.models.SyncedFolder; /** * Class encapsulating common code and references for RemoteContentScanner & LocalContentScanner + * The goal is to generate SyncRequest for file that need to be synchronized * @author vincent Bourgmayer */ -public class AbstractContentScanner { +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 */ - protected AbstractContentScanner(Context context, Account account) { + protected AbstractContentScanner(Context context, Account account, List syncedFolders) { syncRequests = new HashMap<>(); this.context = context; this.account = account; + this.syncedFolders = syncedFolders; + } + + /** + * Method to look for file to synchronize into a given list of files. + * The main logic is fixed but some part depend of the implementation + * @param fileList List of file, instances of T class + * @param fileStates SyncedFileState representing already known files + * @return HashMap with SynceFileState ID as the key and SyncRequest instance as the value + */ + public final HashMap scanContent(List fileList, List fileStates) { + fileStates.removeIf(p -> p.isMediaType() && p.getName().startsWith(".")); //ignore hidden medias from db + + FileLoop: for (final T file : fileList) { + final ListIterator iterator = fileStates.listIterator(); + + while (iterator.hasNext()) { + final SyncedFileState fileState = iterator.next(); + if (isFileMatchingSyncedFileState(file, fileState)) { + onKnownFileFound(file, fileState); + iterator.remove(); + continue FileLoop; + } + } + onNewFileFound(file); + } + + for(SyncedFileState remainingFileState : fileStates) { + onMissingRemoteFile(remainingFileState); + } + return syncRequests; + }; + + /** + * Obtain the SyncedFolder for parent of file denoted by the given path + * The method to obtain syncedFolder depend of implementation of ContentScanner + * @param filePath path of the file for which we want a syncFolder + * @return SyncedFolder instance if found or null + */ + protected SyncedFolder getParentSyncedFolder(String filePath) { + final String dirPath = filePath.substring(0, filePath.lastIndexOf(FileUtils.PATH_SEPARATOR) + 1); + + for(SyncedFolder syncedFolder : syncedFolders) { + if (isSyncedFolderParentOfFile(syncedFolder, dirPath)) { + return syncedFolder; + } + } + return null; } + + /** + * 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); + + /** + * A new file has been found + * - Create SyncedFileState for it and insert in DB + * - Create a syncRequest for it + * @param file The new remote file + */ + protected abstract void onNewFileFound(T file); + + /** + * A known file has been found + * Check what to do: ignore, update Database with missing input or create a new syncRequest + * @param file The remote file + * @param fileState file's latest known state + */ + protected abstract void onKnownFileFound(T file, SyncedFileState fileState); + protected abstract boolean isFileMatchingSyncedFileState(T file, SyncedFileState fileState); + protected abstract boolean isSyncedFolderParentOfFile(SyncedFolder syncedFolder, String dirPath); } diff --git a/app/src/main/java/foundation/e/drive/contentScanner/LocalContentScanner.java b/app/src/main/java/foundation/e/drive/contentScanner/LocalContentScanner.java new file mode 100644 index 0000000000000000000000000000000000000000..5864c980b8d93fa337c9b20e0c00559f1c61c9a6 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/contentScanner/LocalContentScanner.java @@ -0,0 +1,99 @@ +/* + * 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.accounts.Account; +import android.content.Context; +import android.util.Log; + +import java.io.File; +import java.util.List; + +import foundation.e.drive.database.DbHelper; +import foundation.e.drive.models.SyncRequest; +import foundation.e.drive.models.SyncedFileState; +import foundation.e.drive.models.SyncedFolder; +import foundation.e.drive.utils.CommonUtils; +import foundation.e.drive.utils.FileDiffUtils; + +/** + * Class to encapsulate function about scanning local file and + * create syncRequest when needed + */ +public class LocalContentScanner extends AbstractContentScanner{ + + private static final String TAG = LocalContentScanner.class.getSimpleName(); + + public LocalContentScanner(Context context, Account account, List syncedFolders) { + super(context, account, syncedFolders); + } + + @Override + protected void onMissingRemoteFile(SyncedFileState fileState) { + if (!fileState.hasBeenSynchronizedOnce()) { + return; + } + + final File file = new File(fileState.getLocalPath()); + + if (file.exists()) { + Log.w(TAG, "Expected " + file.getAbsolutePath() + "to be missing. but it still exists"); + return; + } + + Log.i(TAG, "Add remove SyncRequest for file " + file.getAbsolutePath()); + syncRequests.put(fileState.getId(), new SyncRequest(fileState, SyncRequest.Type.REMOTE_DELETE)); + } + + @Override + protected void onNewFileFound(File file) { + final String filePath = file.getAbsolutePath(); + final SyncedFolder parentDir = getParentSyncedFolder(filePath); + if (parentDir == null) return; + + int scannableValue = 0; + if (parentDir.isEnabled()) { + if (parentDir.isScanRemote()) scannableValue++; + 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){ + newSyncedFileState.setId( storedId ); + Log.i(TAG, "Add upload SyncRequest for new file " + filePath); + syncRequests.put(storedId, new SyncRequest(newSyncedFileState, SyncRequest.Type.UPLOAD)); + } else { + Log.w(TAG, "Failed to insert (in DB) new SyncedFileState for " + filePath); + } + } + + @Override + protected void onKnownFileFound(File file, SyncedFileState fileState) { + if (FileDiffUtils.getActionForFileDiff(file, fileState) == FileDiffUtils.Action.Upload) { + Log.d(TAG, "Add upload SyncRequest for " + file.getAbsolutePath()); + syncRequests.put(fileState.getId(), new SyncRequest(fileState, SyncRequest.Type.UPLOAD)); + } + } + + @Override + protected boolean isSyncedFolderParentOfFile(SyncedFolder syncedFolder, String dirPath) { + return syncedFolder.getLocalFolder().equals(dirPath); + } + + @Override + protected boolean isFileMatchingSyncedFileState(File file, SyncedFileState fileState) { + final String filePath = CommonUtils.getLocalPath(file); + return fileState.getLocalPath().equals(filePath); + } +} + + 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 65fa1dcfb64ac93ac946f71404a7f082da003346..4f424bb2b517ecc6dc70209160f58d0996fbd450 100644 --- a/app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java +++ b/app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java @@ -14,96 +14,52 @@ import android.content.Context; import android.provider.MediaStore; import android.util.Log; -import com.owncloud.android.lib.resources.files.FileUtils; import com.owncloud.android.lib.resources.files.model.RemoteFile; import java.io.File; -import java.util.HashMap; import java.util.List; -import java.util.ListIterator; 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; import foundation.e.drive.utils.FileDiffUtils; /** - * Class encapsulating code for scaning remote content + * Implementation of AbstractContentScanner for RemoteFile * @author vincent Bourgmayer */ -public class RemoteContentScanner extends AbstractContentScanner { +public class RemoteContentScanner extends AbstractContentScanner { private static final String TAG = RemoteContentScanner.class.getSimpleName(); - private List syncedFolders; - /** - * * @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 syncedFolder) { - super(context, account); - this.syncedFolders = syncedFolder; + public RemoteContentScanner(Context context, Account account, List syncedFolders) { + super(context, account, syncedFolders); } - public HashMap scanContent(List remoteFiles, List fileStates) { - - fileStates.removeIf(p -> p.isMediaType() && p.getName().startsWith(".")); //ignore hidden medias from db - - remoteFileLoop: for (final RemoteFile file : remoteFiles) { - final String remoteFilePath = file.getRemotePath(); - final ListIterator iterator = fileStates.listIterator(); - - while (iterator.hasNext()) { - final SyncedFileState fileState = iterator.next(); - if (fileState.getRemotePath().equals(remoteFilePath)) { - onKnownFileFound(file, fileState); - iterator.remove(); - continue remoteFileLoop; - } - } - - onNewFileFound(file); - } - //At this step, we finished to handle each remote file and we may still have synced file but without remote equivalent. - // In most cases, we consider those files as remotly removed files. So we start to delete those local file. - for (SyncedFileState remainingFileState : fileStates) { - onMissingRemoteFile(remainingFileState); - } - return syncRequests; - } - - /** - * A known file has been found - * Check what to do: ignore, update Database with missing input or create a new DownloadOperation - * @param file The remote file - * @param fileState file's latest known state - */ - private void onKnownFileFound(RemoteFile file, SyncedFileState fileState) { + @Override + protected void onKnownFileFound(RemoteFile file, SyncedFileState fileState) { final FileDiffUtils.Action action = getActionForFileDiff(file, fileState); if (action == FileDiffUtils.Action.Download) { - this.syncRequests.put(fileState.getId(), new DownloadRequest(file, fileState)); + Log.d(TAG, "Add download SyncRequest for " + file.getRemotePath()); + syncRequests.put(fileState.getId(), new DownloadRequest(file, fileState)); } else if (action == FileDiffUtils.Action.updateDB) { fileState.setLastETAG(file.getEtag()); final int affectedRows = DbHelper.manageSyncedFileStateDB(fileState, "UPDATE", context); - if (affectedRows == 0) Log.e(TAG, "Error while updating eTag in DB for: " + file); + if (affectedRows == 0) Log.e(TAG, "Error while updating eTag in DB for: " + file.getRemotePath()); } } - /** - * A new remote file has been found - * - Create SyncedFileState for it and insert in DB - * - Create a Download syncRequest for it - * @param file The new remote file - */ - private void onNewFileFound(RemoteFile file) { + @Override + protected void onNewFileFound(RemoteFile file) { final String remoteFilePath = file.getRemotePath(); final SyncedFolder parentDir = getParentSyncedFolder(remoteFilePath); if (parentDir == null) return; @@ -122,16 +78,15 @@ public class RemoteContentScanner extends AbstractContentScanner { final int storedId = DbHelper.manageSyncedFileStateDB(newFileState, "INSERT", context); if (storedId > 0) { newFileState.setId(storedId); + Log.d(TAG, "Add downloadSyncRequest for new remote file: " + remoteFilePath); this.syncRequests.put(storedId, new DownloadRequest(file, newFileState)); + } else { + Log.w(TAG, "Failed to insert (in DB) new SyncedFileState for remote file " + remoteFilePath); } } - - /** - * When a remoteFile doesn't exist anymore we remove it from device & from Database - * @param fileState SyncedFileState for which we lack remote file - */ - private void onMissingRemoteFile(SyncedFileState fileState) { + @Override + protected void onMissingRemoteFile(SyncedFileState fileState) { if (!CommonUtils.isThisSyncAllowed(account, fileState.isMediaType())) { Log.d(TAG, "Sync of current file: " + fileState.getName() + " isn't allowed"); return; @@ -161,20 +116,13 @@ public class RemoteContentScanner extends AbstractContentScanner { } } - /** - * Get SyncedFolder corresponding to parent of remotefile - * @param filePath Remote file path - * @return SyncedFolder or null if none have been found - */ - private SyncedFolder getParentSyncedFolder(String filePath) { - - final String dirPath = filePath.substring(0, filePath.lastIndexOf(FileUtils.PATH_SEPARATOR) + 1); + @Override + protected boolean isFileMatchingSyncedFileState(RemoteFile file, SyncedFileState fileState) { + return fileState.getRemotePath().equals(file.getRemotePath()); + } - for (SyncedFolder syncedFolder : syncedFolders) { - if (syncedFolder.getRemoteFolder().equals(dirPath)) { - return syncedFolder; - } - } - return null; + @Override + protected boolean isSyncedFolderParentOfFile(SyncedFolder syncedFolder, String dirPath) { + return syncedFolder.getRemoteFolder().equals(dirPath); } } 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 d6bbb566b1a5ac58ab81c58c2fe818f63f514bf3..ae3716dccee39eaceed5e51423947191361010fc 100644 --- a/app/src/main/java/foundation/e/drive/services/ObserverService.java +++ b/app/src/main/java/foundation/e/drive/services/ObserverService.java @@ -40,6 +40,7 @@ import java.util.HashMap; import java.util.List; import java.util.ListIterator; +import foundation.e.drive.contentScanner.LocalContentScanner; import foundation.e.drive.contentScanner.RemoteContentScanner; import foundation.e.drive.database.DbHelper; import foundation.e.drive.fileFilters.CrashlogsFileFilter; @@ -500,116 +501,11 @@ public class ObserverService extends Service implements OnRemoteOperationListene folderIdList); if (!syncedFileStates.isEmpty() || !fileList.isEmpty() ) { - handleLocalFiles(fileList, syncedFileStates); + final LocalContentScanner scanner= new LocalContentScanner(getApplicationContext(), mAccount, mSyncedFolders); + syncRequests.putAll(scanner.scanContent(fileList, syncedFileStates)); } } } - - /** - * This function determine the action to do depending for each file or syncedFileState - * If file has already be synced and modified since last synced then update (= upload) - * if file has never been synced then upload - * if file has already be on server once(=> SyncedFIleState.etag is valid) and not local file exist then remove syncedFile - * @param localFileList list of local file to scan - * @param syncedFileStates List of SyncedFileState to scan - */ - private void handleLocalFiles(List localFileList, List syncedFileStates ){ - Log.i(TAG, "handleLocalFiles()"); - Log.d(TAG, "Loop through local file list"); - - //Loop through local files - for (int i =-1, localFilesSize = localFileList.size(); ++i < localFilesSize;){ - - final File localFile = localFileList.get(i); - final String filePath = CommonUtils.getLocalPath( localFile ); - boolean correspondant_found = false; - - Log.v(TAG, "Current file is "+filePath+", exist: "+localFile.exists()+", last modified: "+localFile.lastModified() ); - - final ListIterator syncedFileListIterator = syncedFileStates.listIterator(); - Log.d(TAG, "Loop through syncedFileStates "); - - while( syncedFileListIterator.hasNext() ) { - final SyncedFileState syncedFileState = syncedFileListIterator.next(); - - //Ignore hidden media file store in DB - if (syncedFileState.isMediaType() && syncedFileState.getName().startsWith(".")) { - syncedFileListIterator.remove(); - continue; - } - - Log.v(TAG, syncedFileState.getLocalPath()+", "+syncedFileState.getId()+", "+syncedFileState.getLocalLastModified()); - - //if syncedFileState correspond to local file - if (syncedFileState.getLocalPath().equals(filePath)) { - correspondant_found = true; - - if (FileDiffUtils.getActionForFileDiff(localFile, syncedFileState) == Action.Upload) { - this.syncRequests.put(syncedFileState.getId(), new SyncRequest(syncedFileState, SyncRequest.Type.UPLOAD)); - } - syncedFileListIterator.remove(); - break; - } - } - if (correspondant_found) continue; - - Log.v(TAG, "this is a new file to sync"); - - //Extract parent path from knownPath - final String parentPath = filePath.substring(0, filePath.lastIndexOf(FileUtils.PATH_SEPARATOR) + 1); - - //look into synced folders if folder path exist - for(SyncedFolder syncedFolder : mSyncedFolders){ - if (syncedFolder.getLocalFolder().equals(parentPath)){ - int scannableValue = 0; - if (syncedFolder.isEnabled()) { - if (syncedFolder.isScanRemote()) scannableValue++; - if (syncedFolder.isScanLocal()) scannableValue += 2; - } - - //create the syncedFile State - final SyncedFileState newSyncedFileState = new SyncedFileState(-1, localFile.getName(), filePath, syncedFolder.getRemoteFolder() + localFile.getName(), "", 0, syncedFolder.getId(), syncedFolder.isMediaType(),scannableValue); - - //Store it in DB - int storedId = DbHelper.manageSyncedFileStateDB(newSyncedFileState, "INSERT", this); - if (storedId > 0){ - newSyncedFileState.setId( storedId ); - Log.i(TAG, "Add upload operation for new file "+storedId); - - this.syncRequests.put(storedId, new SyncRequest(newSyncedFileState, SyncRequest.Type.UPLOAD)); - } else { - Log.w(TAG, "The new file to synced cannot be store in DB. Ignore it"); - } - break; - } //end of test if folder path match with file's parent path - }//end of loop over folder - }//end of loop over local files - handleLocalRemainingSyncedFileState( syncedFileStates ); - } - - /** - * manage rest of the list of syncedFilesState to look for remote file to delete - * @param syncedFileStates List of SyncedFileState for which no local equivalent has been found - */ - private void handleLocalRemainingSyncedFileState(List syncedFileStates){ - Log.i(TAG, "handleLocalRemainingSyncedFileState(...)"); - - for(SyncedFileState fileState : syncedFileStates) { - if (!fileState.hasBeenSynchronizedOnce()) { - continue; - } - - final File file = new File(fileState.getLocalPath()); - - if (file.exists()) { - Log.w(TAG, file.getAbsolutePath() + "The file still exist. There is a problem!"); - continue; - } - - Log.i(TAG, "Add remove SyncRequest for file " + file.getAbsolutePath()); - this.syncRequests.put(fileState.getId(), new SyncRequest(fileState, SyncRequest.Type.REMOTE_DELETE)); - } - } /* end of methods related to device Scanning */ @Nullable