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

Commit 0755c7f8 authored by Fahim Salam Chowdhury's avatar Fahim Salam Chowdhury 👽
Browse files

1562-Refactor_fileObserver_implementation

parent 02224377
Loading
Loading
Loading
Loading
+19 −30
Original line number Diff line number Diff line
@@ -8,40 +8,40 @@

package foundation.e.drive;

import static timber.log.Timber.DebugTree;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Environment;

import foundation.e.drive.FileObservers.FileEventListener;
import foundation.e.drive.FileObservers.RecursiveFileObserver;
import androidx.annotation.NonNull;

import foundation.e.drive.database.FailedSyncPrefsManager;
import foundation.e.drive.fileObservers.FileObserverManager;
import foundation.e.drive.utils.AppConstants;
import foundation.e.drive.utils.CommonUtils;
import foundation.e.drive.utils.ReleaseTree;
import foundation.e.lib.telemetry.Telemetry;
import timber.log.Timber;
import static timber.log.Timber.DebugTree;

import androidx.annotation.NonNull;

/**
 * Class representing the eDrive application.
 * It is instantiated before any other class.
 *
 * @author Jonathan klee
 * @author Vincent Bourgmayer
 */
public class EdriveApplication extends Application {
    private RecursiveFileObserver mFileObserver = null;
    private FileEventListener fileEventListener;
    private FileObserverManager fileObserverManager = null;

    @Override
    public void onCreate() {
        super.onCreate();
        setupLogging();
        instantiateFileEventListener();

        fileObserverManager = new FileObserverManager(getApplicationContext());

        CommonUtils.createNotificationChannel(getApplicationContext());

@@ -49,7 +49,9 @@ public class EdriveApplication extends Application {

        if (!isAccountStoredInPreferences(prefs)) {
            final Account account = CommonUtils.getAccount(getString(R.string.eelo_account_type), AccountManager.get(this));
            if (account == null) { return; }
            if (account == null) {
                return;
            }

            prefs.edit().putString(AccountManager.KEY_ACCOUNT_NAME, account.name)
                    .putString(AccountManager.KEY_ACCOUNT_TYPE, account.type)
@@ -59,19 +61,12 @@ public class EdriveApplication extends Application {
        FailedSyncPrefsManager.getInstance(getApplicationContext()).clearPreferences();
    }

    /**
     * Start Recursive FileObserver if not already watching
     */
    synchronized public void startRecursiveFileObserver() {
        if (!mFileObserver.isWatching()) {
            mFileObserver.startWatching();
            Timber.d("Started RecursiveFileObserver on root folder");
        }
        fileObserverManager.initializeObserving();
    }

    synchronized public void stopRecursiveFileObserver() {
        mFileObserver.stopWatching();
        Timber.d("Stopped RecursiveFileObserver on root folder");
        fileObserverManager.stopObserving();
    }

    @Override
@@ -80,21 +75,15 @@ public class EdriveApplication extends Application {
        Timber.i("System is low on memory. Application might get killed by the system.");
    }

    private void instantiateFileEventListener() {
        fileEventListener = new FileEventListener(getApplicationContext());

        final String pathForObserver = Environment.getExternalStorageDirectory().getAbsolutePath();
        mFileObserver = new RecursiveFileObserver(getApplicationContext(), pathForObserver, fileEventListener);
    }

    private void setupLogging() {
        if (BuildConfig.DEBUG) {
            Timber.plant(new DebugTree());
        } else {
            return;
        }

        Telemetry.init(BuildConfig.SENTRY_DSN, this, true);
        Timber.plant(new ReleaseTree());
    }
    }

    private boolean isAccountStoredInPreferences(@NonNull SharedPreferences prefs) {
        return prefs.getString(AccountManager.KEY_ACCOUNT_NAME, null) != null;
+0 −243
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.FileObservers;

import static foundation.e.drive.models.SyncRequest.Type.DISABLE_SYNCING;
import static foundation.e.drive.models.SyncRequest.Type.UPLOAD;
import static foundation.e.drive.models.SyncedFileStateKt.DO_NOT_SCAN;
import static foundation.e.drive.models.SyncedFileStateKt.SCAN_ON_CLOUD;
import static foundation.e.drive.models.SyncedFileStateKt.SCAN_ON_DEVICE;
import static foundation.e.drive.utils.FileUtils.getLocalPath;

import android.content.Context;
import android.os.FileObserver;

import androidx.annotation.NonNull;

import static com.owncloud.android.lib.resources.files.FileUtils.PATH_SEPARATOR;

import java.io.File;

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.synchronization.SyncRequestCollector;
import foundation.e.drive.synchronization.SyncProxy;
import timber.log.Timber;

/**
 * @author Narinder Rana
 * @author vincent Bourgmayer
 */
public class FileEventListener  {

    private final Context appContext;

    public FileEventListener(@NonNull Context applicationContext) {
        Timber.tag(FileEventListener.class.getSimpleName());
        this.appContext = applicationContext;
    }

    public void onEvent(int event, @NonNull File file) {
        if (file.isHidden()) return;

        if (file.isDirectory()) {
            handleDirectoryEvent(event, file);
        } else {
            handleFileEvent(event, file);
        }
    }

    /**
     * Handle some file event for a file which is not a directory
     * @param event the event mask. CLOSE_WRITE, DELETE & MOVE_SELF are handled
     * @param file the file concerned by the event
     */
    private void handleFileEvent(int event, @NonNull File file) {
        switch(event) {
            case FileObserver.CLOSE_WRITE: //todo it is called two times per file except if screenshot by example or take a picture
                handleFileCloseWrite(file);
                break;
            case FileObserver.DELETE:
                handleFileDelete(file);
                break;
            case FileObserver.MOVE_SELF: //todo to be able to catch that, we probably need a buffer to catch a succession (MOVE_FROM, MOVE_TO, then MOVE_SELF).
                Timber.d("%s has been moved. Not handled yet", file.getAbsolutePath());
                break;
            default:
                break;
        }
    }

    /**
     * Handle FileEvent for a directory
     * @param event FileEvent mask. CREATE, CLOSE_WRITE, DELETE, MOVE_SELF
     * @param dir directory concerned by file event
     */
    private void handleDirectoryEvent(int event, @NonNull File dir) {
        switch(event) {
            case FileObserver.CREATE:
                handleDirectoryCreate(dir);
                break;
            case FileObserver.CLOSE_WRITE:
                handleDirectoryCloseWrite(dir);
                break;
            case FileObserver.DELETE: //todo #1 Fix: never used. when done on a dir, it triggers handleFileEvent. Why ?!
                handleDirectoryDelete(dir);
                break;
            case FileObserver.MOVE_SELF:
                Timber.d("%s has been moved. Not handled yet", dir.getAbsolutePath());
                break;
            default:
                break;
        }
    }

    /**
     * Send syncRequest to SynchronizationService
     * @param request SyncRequest that should be executed asap
     */
    private void sendSyncRequestToSynchronizationService(@NonNull SyncRequest request) {
        final SyncRequestCollector syncManager = SyncProxy.INSTANCE;
        final boolean requestAdded = syncManager.queueSyncRequest(request, appContext.getApplicationContext());
        if (requestAdded) {
            Timber.d("Sending a SyncRequest for %s", request.getSyncedFileState().getName());
            syncManager.startSynchronization(appContext);
        }
    }

    /**
     * When a new directory is detected, it must be inserted in database
     * if it's parent directory is already in the database
     * @param directory Directory that has been created
     */
    private void handleDirectoryCreate(@NonNull File directory) {
        Timber.d("handleDirectoryCreate( %s )",directory.getAbsolutePath());
        final File parentFile = directory.getParentFile();
        if (parentFile == null) return;

        final String parentPath = getLocalPath(parentFile);
        final SyncedFolder parentFolder = DbHelper.getSyncedFolderByLocalPath(parentPath, appContext);
        if (parentFolder != null) {
            final SyncedFolder folder = new SyncedFolder(parentFolder, directory.getName() + PATH_SEPARATOR, directory.lastModified(), "");
            DbHelper.insertSyncedFolder(folder, appContext);
        }
    }

    /**
     * Handle CLOSE_WRITE event for a directory
     * todo: check in which condition a directory can generate a close_write
     * @param directory Directory that has been modified
     */
    private void handleDirectoryCloseWrite(@NonNull File directory) {
        final String fileLocalPath = getLocalPath(directory);
        Timber.d("handleDirectoryCloseWrite( %s )",fileLocalPath );
        final SyncedFolder folder = DbHelper.getSyncedFolderByLocalPath(fileLocalPath, appContext);
        if (folder == null) {
            handleDirectoryCreate(directory); //todo check if really relevant
        } else {  //It's a directory update
            folder.setLastModified(directory.lastModified());
            DbHelper.updateSyncedFolder(folder, appContext);
        }
    }

    /**
     * Handle a file deletion event for a directory
     * @param directory Directory that has been removed
     */
    private void handleDirectoryDelete(@NonNull File directory) {
        final String fileLocalPath = getLocalPath(directory);
        Timber.d("handleDirectoryDelete( %s )", fileLocalPath);
        SyncedFolder folder = DbHelper.getSyncedFolderByLocalPath(fileLocalPath, appContext);
        if (folder == null) {
            //look for parent
            final File parentFile = directory.getParentFile();
            if (parentFile == null) return;

            final String parentPath = getLocalPath(parentFile);
            final SyncedFolder parentFolder = DbHelper.getSyncedFolderByLocalPath(parentPath, appContext);
            if (parentFolder == null ) { //if parent is not in the DB
                return;
            }
            folder = new SyncedFolder(parentFolder, directory.getName() + PATH_SEPARATOR, directory.lastModified(), "");
            folder.setEnabled(false);
            DbHelper.insertSyncedFolder(folder, appContext);
        } else if (folder.isEnabled()) {
            folder.setEnabled(false);
            DbHelper.updateSyncedFolder(folder, appContext);
        }
    }

    /**
     * handle a file close_write event for a file which is not a directory
     * @param file File that has been modified
     */
    private void handleFileCloseWrite(@NonNull File file) {
        final String fileLocalPath = getLocalPath(file);
        Timber.d("handleFileCloseWrite( %s )", fileLocalPath);
        SyncRequest request = null;
        SyncedFileState fileState = DbHelper.loadSyncedFile( appContext, fileLocalPath, true);

        if (fileState == null) { //New file discovered
            final File parentFile = file.getParentFile();
            if (parentFile == null) return;

            final String parentPath = getLocalPath(file.getParentFile());
            SyncedFolder parentFolder = DbHelper.getSyncedFolderByLocalPath(parentPath, appContext);
            if (parentFolder == null || !parentFolder.isEnabled()) {
                Timber.d("Won't send sync request: no parent are known for new file: %s", file.getName());
                return;
            }
            int scanScope = DO_NOT_SCAN;
            if (parentFolder.isEnabled()) {
                if (parentFolder.isScanRemote()) scanScope = SCAN_ON_CLOUD;
                if (parentFolder.isScanLocal()) scanScope += SCAN_ON_DEVICE;
            }

            final String remotePath = parentFolder.getRemoteFolder()+file.getName();
            fileState = new SyncedFileState(-1, file.getName(), getLocalPath(file), remotePath, "", 0L, parentFolder.getId(), parentFolder.isMediaType(), scanScope);
            int storedId = DbHelper.manageSyncedFileStateDB(fileState, "INSERT", appContext);
            if (storedId > 0) {
                fileState.setId(storedId);
                request = new SyncRequest(fileState, UPLOAD);
            } else {
                Timber.d("New File %s observed but impossible to insert it in DB", file.getName());
            }
        } else { //File update
            final boolean isWaitingForDownload = fileState.isLastEtagStored() && fileState.getLastModified() == 0L;
            if (fileState.getScanScope() > SCAN_ON_CLOUD && !isWaitingForDownload) {
                request = new SyncRequest(fileState, UPLOAD);
            }
        }
        if (request != null) {
            sendSyncRequestToSynchronizationService(request);
        }
    }

    /**
     * Handle a file deletion event for a file which is not a directory
     * @param file File that has been removed
     */
    private void handleFileDelete(@NonNull File file) {
        final String fileLocalPath = getLocalPath(file);
        Timber.d("handleFileDelete( %s )",fileLocalPath);
        final SyncedFileState fileState = DbHelper.loadSyncedFile( appContext, fileLocalPath, true);
        if (fileState == null) {
            return; //Todo #1: should we call handleDirectoryDelete before to return ?
        }

        //If already in DB
        if (fileState.getScanScope() > DO_NOT_SCAN) {
            //todo: if file is already sync disabled, we should probably remove file from DB
            final SyncRequest disableSyncingRequest = new SyncRequest(fileState, DISABLE_SYNCING);
            this.sendSyncRequestToSynchronizationService(disableSyncingRequest);
        }
    }
}
+0 −179
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.FileObservers;

import android.content.Context;
import android.os.FileObserver;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.io.File;
import java.io.FileFilter;
import java.util.HashMap;
import java.util.List;
import java.util.Stack;

import foundation.e.drive.database.DbHelper;
import foundation.e.drive.models.SyncedFolder;

/**
 * @author Narinder Rana
 * @author Vincent Bourgmayer
 */
public class RecursiveFileObserver extends FileObserver {
    private final HashMap<String, FileObserver> observers = new HashMap<>();
    // protected to avoid SyntheticAccessor
    protected static final  FileFilter WATCHABLE_DIRECTORIES_FILTER = new FileFilter() {
        @Override
        public boolean accept(File file) {
            return file.isDirectory() && !file.getName().startsWith(".");
        }
    };

    private boolean watching = false;
    private final Context applicationContext;
    private String path;
    private int mask;
    private FileEventListener listener;

    public RecursiveFileObserver(@NonNull Context applicationContext, @NonNull String path, @Nullable FileEventListener listener) {
        this(applicationContext, path, ALL_EVENTS, listener);
    }

    public RecursiveFileObserver(@NonNull Context applicationContext, @NonNull String path, int mask, @Nullable FileEventListener listener) {
        super(path, mask);
        this.path = path;
        this.mask = mask | FileObserver.CREATE | FileObserver.DELETE_SELF;
        this.listener = listener;
        this.applicationContext = applicationContext;
    }


    @Override
    public void onEvent(int event, @Nullable String path) {
        File file;
        if (path == null) {
            file = new File(this.path);
        } else {
            file = new File(this.path, path);
        }

        notify(event, file);
    }

    // protected to avoid SyntheticAccessor
    protected void notify(int event, @NonNull File file) {
        if (listener != null) {
            listener.onEvent(event & FileObserver.ALL_EVENTS, file);
        }
    }

    @Override
    public void startWatching() {
        Stack<String> stack = new Stack<>();

        List<SyncedFolder> mSyncedFolders = DbHelper.getAllSyncedFolders(applicationContext);
        if (!mSyncedFolders.isEmpty()){
            for (SyncedFolder syncedFolder:mSyncedFolders){
                stack.push(syncedFolder.getLocalFolder());
                stack.push(syncedFolder.getRemoteFolder());
            }
            watching = true;
        }

        // Recursively watch all child directories
        while (!stack.empty()) {
            String parent = stack.pop();
            startWatching(parent);

            File path = new File(parent);
            File[] files = path.listFiles(WATCHABLE_DIRECTORIES_FILTER);
            if (files != null) {
                for (File file : files) {
                    stack.push(file.getAbsolutePath());
                }
            }
        }
    }

    /**
     * Start watching a single file
     * @param path
     */
    protected void startWatching(@NonNull String path) {
        synchronized (observers) {
            FileObserver observer = observers.remove(path);
            if (observer != null) {
                observer.stopWatching();
            }
            observer = new SingleFileObserver(path, mask);
            observer.startWatching();
            observers.put(path, observer);
        }
    }

    @Override
    public void stopWatching() {
        for (FileObserver observer : observers.values()) {
            observer.stopWatching();
        }
        observers.clear();
        watching = false;
    }

    /**
     * Stop watching a single file
     * @param path
     */
    protected void stopWatching(@NonNull String path) {
        synchronized (observers) {
            FileObserver observer = observers.remove(path);
            if (observer != null) {
                observer.stopWatching();
            }
        }
    }

    public boolean isWatching(){
        return watching;
    }

    private class SingleFileObserver extends FileObserver {
        private String filePath;

        public SingleFileObserver(String path, int mask) {
            super(path, mask);
            filePath = path;
        }

        @Override
        public void onEvent(int event, @Nullable String path) {
            File file;
            if (path == null) {
                file = new File(filePath);
            } else {
                file = new File(filePath, path);
            }

            switch (event & FileObserver.ALL_EVENTS) {
                case DELETE_SELF:
                    RecursiveFileObserver.this.stopWatching(filePath);
                    break;
                case CREATE:
                    if (WATCHABLE_DIRECTORIES_FILTER.accept(file)) {
                        RecursiveFileObserver.this.startWatching(file.getAbsolutePath());
                    }
                    break;
            }

            RecursiveFileObserver.this.notify(event, file);
        }
    }
}
+41 −0
Original line number Diff line number Diff line
/*
 * Copyright © MURENA SAS 2023.
 * 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.fileObservers

import android.os.Build
import android.os.FileObserver
import androidx.annotation.RequiresApi
import foundation.e.drive.synchronization.StateMachine
import foundation.e.drive.synchronization.SyncState
import java.io.File

@RequiresApi(Build.VERSION_CODES.Q)
class DirectoryObserver(
    private val dirPath: String,
    private val listener: FileEventListener
) : FileObserver(File(dirPath), EVENT_MASKS) {

    companion object {
        const val EVENT_MASKS = (CLOSE_WRITE or CREATE
                or MOVED_FROM or MOVED_TO or MOVE_SELF
                or DELETE_SELF or DELETE)
    }

    override fun onEvent(event: Int, path: String?) {
        val syncState = StateMachine.currentState
        if (syncState != SyncState.LISTENING_FILES) {
            return
        }

        val file = if (path == null) File(dirPath) else File(dirPath, path)

        // to retrieve actual event code, we need to apply conjunction operation with the all event code
        listener.notify(event and ALL_EVENTS, file, dirPath)
    }
}
+244 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading