Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 650ae8dd authored by Vincent Bourgmayer's avatar Vincent Bourgmayer
Browse files

Refactor ObserverService to make it better understandable. The goal is also to...

Refactor ObserverService to make it better understandable. The goal is also to make him fetch clean code design principle.

- Create new package 'contentScanner'
- Create 'foundation.e.drive.contentScanner.AbstractContentScanner' and subclass for Remote & local files scanning
- Move codes from ObserverService into contentScanner classes
- Create FileDiffUtils class to extract more codes from ObserverService
parent cd7c3e9c
Loading
Loading
Loading
Loading
+115 −0
Original line number Diff line number Diff line
/*
 * 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 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 abstract class AbstractContentScanner<T> {
    protected final Context context;
    protected final Account account;
    protected final HashMap<Integer, SyncRequest> syncRequests;
    protected final List<SyncedFolder> 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, List<SyncedFolder> 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<Integer, SyncRequest> with SynceFileState ID as the key and SyncRequest instance as the value
     */
    public final HashMap<Integer, SyncRequest> scanContent(List<T> fileList, List<SyncedFileState> fileStates) {
        fileStates.removeIf(p -> p.isMediaType() && p.getName().startsWith(".")); //ignore hidden medias from db

        FileLoop: for (final T file : fileList) {
            final ListIterator<SyncedFileState> 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);
}
+99 −0
Original line number Diff line number Diff line
/*
 * 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<File>{

    private static final String TAG = LocalContentScanner.class.getSimpleName();

    public LocalContentScanner(Context context, Account account, List<SyncedFolder> 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);
    }
}

+128 −0
Original line number Diff line number Diff line
/*
 * 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 foundation.e.drive.utils.FileDiffUtils.getActionForFileDiff;

import android.accounts.Account;
import android.content.Context;
import android.provider.MediaStore;
import android.util.Log;

import com.owncloud.android.lib.resources.files.model.RemoteFile;

import java.io.File;
import java.util.List;

import foundation.e.drive.database.DbHelper;
import foundation.e.drive.models.DownloadRequest;
import foundation.e.drive.models.SyncedFileState;
import foundation.e.drive.models.SyncedFolder;
import foundation.e.drive.utils.CommonUtils;
import foundation.e.drive.utils.FileDiffUtils;

/**
 * Implementation of AbstractContentScanner for RemoteFile
 * @author vincent Bourgmayer
 */
public class RemoteContentScanner extends AbstractContentScanner<RemoteFile> {
    private static final String TAG = RemoteContentScanner.class.getSimpleName();

    /**
     * @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> syncedFolders) {
        super(context, account, syncedFolders);
    }

    @Override
    protected void onKnownFileFound(RemoteFile file, SyncedFileState fileState) {
        final FileDiffUtils.Action action = getActionForFileDiff(file, fileState);
        if (action == FileDiffUtils.Action.Download) {

            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.getRemotePath());

        }
    }

    @Override
    protected void onNewFileFound(RemoteFile file) {
        final String remoteFilePath = file.getRemotePath();
        final SyncedFolder parentDir = getParentSyncedFolder(remoteFilePath);
        if (parentDir == null) return;

        final String fileName = CommonUtils.getFileNameFromPath(remoteFilePath);

        int scannableValue = 0;
        if (parentDir.isEnabled()) {
            if (parentDir.isScanRemote()) scannableValue++;
            if (parentDir.isScanLocal()) scannableValue += 2;
        }

        final SyncedFileState newFileState = new SyncedFileState(-1, fileName, parentDir.getLocalFolder() + fileName, remoteFilePath, file.getEtag(), 0, parentDir.getId(), parentDir.isMediaType(), scannableValue);

        //Store it in DB
        final int storedId = DbHelper.manageSyncedFileStateDB(newFileState, "INSERT", context);
        if (storedId > 0) {
            newFileState.setId(storedId);
            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);
        }
    }

    @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;
        }

        //Check that file has already been synced fully
        if (!fileState.hasBeenSynchronizedOnce()) {
            return;
        }

        final File file = new File(fileState.getLocalPath());
        if (!file.exists()) {
            return;
        }

        context.getContentResolver().delete(MediaStore.Files.getContentUri("external"),
                MediaStore.Files.FileColumns.DATA + "=?",
                new String[]{CommonUtils.getLocalPath(file)});

        if (!file.delete()) { //May throw SecurityException or IOException
            Log.w(TAG, "local file (" + file.getName() + ") removal failed.");
            return;
        }

        if (DbHelper.manageSyncedFileStateDB(fileState, "DELETE", context) <= 0) {
            Log.e(TAG, "Failed to remove " + file.getName() + " from DB");
        }
    }

    @Override
    protected boolean isFileMatchingSyncedFileState(RemoteFile file, SyncedFileState fileState) {
        return fileState.getRemotePath().equals(file.getRemotePath());
    }

    @Override
    protected boolean isSyncedFolderParentOfFile(SyncedFolder syncedFolder, String dirPath) {
        return syncedFolder.getRemoteFolder().equals(dirPath);
    }
}
+8 −1
Original line number Diff line number Diff line
@@ -23,7 +23,6 @@ public class SyncedFileState implements Parcelable {
    public static final int DEVICE_SCANNABLE=2;
    public static final int ALL_SCANNABLE=3;

    protected SyncedFileState(){}; //@ToRemove. Test Only. It's to allow to make a mock SyncedFileState Class in test.
    private int id;
    private String name; //name of the file
    private String localPath; //Path on the device file system
@@ -144,6 +143,14 @@ public class SyncedFileState implements Parcelable {
        return (this.lastETAG != null && !this.lastETAG.isEmpty() );
    }

    /**
     * Determine in it has already been synchronized once.
     * @return true if contains data for both local (local last modified) & remote file (eTag)
     */
    public boolean hasBeenSynchronizedOnce() {
        return this.isLastEtagStored() && this.getLocalLastModified() > 0L;
    }

    /**
     * Get the syncedFolder _id
     * @return long
+92 −344

File changed.

Preview size limit exceeded, changes collapsed.

Loading