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

Commit 7f7324dd authored by Vincent Bourgmayer's avatar Vincent Bourgmayer
Browse files

Merge branch '144-o-refactor-ObserverService' into 'v1-oreo'

Resolve "Refactor ObserverService"

Closes #144

See merge request !150
parents cd7c3e9c 650ae8dd
Loading
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