diff --git a/app/src/main/java/foundation/e/drive/EdriveApplication.java b/app/src/main/java/foundation/e/drive/EdriveApplication.java index fe9b303fa45e39dc128db77f2cf5da233227753b..853607358fa03a933ba38ca35b9b328df0dcd970 100644 --- a/app/src/main/java/foundation/e/drive/EdriveApplication.java +++ b/app/src/main/java/foundation/e/drive/EdriveApplication.java @@ -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,23 +75,17 @@ 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 { - Telemetry.init(BuildConfig.SENTRY_DSN, this, true); - Timber.plant(new ReleaseTree()); + 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; } -} \ No newline at end of file +} diff --git a/app/src/main/java/foundation/e/drive/FileObservers/FileEventListener.java b/app/src/main/java/foundation/e/drive/FileObservers/FileEventListener.java deleted file mode 100644 index 1d5a9c506f6142e8c19ac9f35ce16460bf67ed48..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/drive/FileObservers/FileEventListener.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * 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); - } - } -} diff --git a/app/src/main/java/foundation/e/drive/FileObservers/RecursiveFileObserver.java b/app/src/main/java/foundation/e/drive/FileObservers/RecursiveFileObserver.java deleted file mode 100644 index 3095537c766e4f8ad4280296a45231be09ebfbc7..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/drive/FileObservers/RecursiveFileObserver.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * 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 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 stack = new Stack<>(); - - List 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); - } - } -} diff --git a/app/src/main/java/foundation/e/drive/fileObservers/DirectoryObserver.kt b/app/src/main/java/foundation/e/drive/fileObservers/DirectoryObserver.kt new file mode 100644 index 0000000000000000000000000000000000000000..30ef8055f8b8a2cf09cf5d9f7f7be946f04babad --- /dev/null +++ b/app/src/main/java/foundation/e/drive/fileObservers/DirectoryObserver.kt @@ -0,0 +1,41 @@ +/* + * 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) + } +} diff --git a/app/src/main/java/foundation/e/drive/fileObservers/FileEventHandler.kt b/app/src/main/java/foundation/e/drive/fileObservers/FileEventHandler.kt new file mode 100644 index 0000000000000000000000000000000000000000..e93ea972574861380c14fd5e753f410e7a7c975e --- /dev/null +++ b/app/src/main/java/foundation/e/drive/fileObservers/FileEventHandler.kt @@ -0,0 +1,244 @@ +/* + * Copyright © MURENA SAS 2023. + * 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 com.owncloud.android.lib.resources.files.FileUtils.PATH_SEPARATOR +import foundation.e.drive.database.DbHelper +import foundation.e.drive.models.DO_NOT_SCAN +import foundation.e.drive.models.SCAN_ON_CLOUD +import foundation.e.drive.models.SCAN_ON_DEVICE +import foundation.e.drive.models.SyncRequest +import foundation.e.drive.models.SyncedFileState +import foundation.e.drive.models.SyncedFolder +import foundation.e.drive.synchronization.SyncProxy +import foundation.e.drive.utils.FileUtils.getLocalPath +import timber.log.Timber +import java.io.File + +/** + * @author Narinder Rana + * @author vincent Bourgmayer + * @author Fahim Salam Chowdhury + */ +class FileEventHandler(private val context: Context) { + + fun onEvent(event: Int, file: File) { + if (file.isDirectory) { + handleDirectoryEvent(event, file) + return + } + + 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 fun handleFileEvent(event: Int, file: File) { + when (event) { + FileObserver.CLOSE_WRITE -> handleFileCloseWrite(file) + FileObserver.MOVED_TO -> handleFileMoveTo(file) + FileObserver.DELETE -> handleFileDelete(file) + } + } + + /** + * Handle FileEvent for a directory + * @param event FileEvent mask. CREATE, CLOSE_WRITE, DELETE, MOVE_SELF + * @param directory directory concerned by file event + */ + private fun handleDirectoryEvent(event: Int, directory: File) { + when (event) { + FileObserver.CREATE -> handleDirectoryCreate(directory) + FileObserver.CLOSE_WRITE -> handleDirectoryCloseWrite(directory) + FileObserver.DELETE, FileObserver.DELETE_SELF -> handleDirectoryDelete(directory) + } + } + + /** + * Send syncRequest to SynchronizationService + * @param request SyncRequest that should be executed asap + */ + private fun sendSyncRequestToSynchronizationService(request: SyncRequest) { + val requestAdded = SyncProxy.queueSyncRequest(request, context.applicationContext) + if (requestAdded) { + Timber.d("Sending a SyncRequest for ${request.syncedFileState.name}") + SyncProxy.startSynchronization(context) + } + } + + /** + * 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 fun handleDirectoryCreate(directory: File) { + Timber.d("handleDirectoryCreate( ${directory.absolutePath} )") + val parentFile = directory.parentFile ?: return + + val parentPath = getLocalPath(parentFile) + val parentFolder = DbHelper.getSyncedFolderByLocalPath(parentPath, context) ?: return + val folder = SyncedFolder( + parentFolder, + directory.name + PATH_SEPARATOR, + directory.lastModified(), + "" + ) + DbHelper.insertSyncedFolder(folder, context) + } + + /** + * 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 fun handleDirectoryCloseWrite(directory: File) { + val fileLocalPath = getLocalPath(directory) + Timber.d("handleDirectoryCloseWrite( $fileLocalPath )") + val folder = DbHelper.getSyncedFolderByLocalPath(fileLocalPath, context) + if (folder == null) { + handleDirectoryCreate(directory) //todo check if really relevant + } else { //It's a directory update + folder.setLastModified(directory.lastModified()) + DbHelper.updateSyncedFolder(folder, context) + } + } + + /** + * Handle a file deletion event for a directory + * @param directory Directory that has been removed + */ + private fun handleDirectoryDelete(directory: File) { + val fileLocalPath = getLocalPath(directory) + Timber.d("handleDirectoryDelete( $fileLocalPath )") + val folder = DbHelper.getSyncedFolderByLocalPath(fileLocalPath, context) + if (folder == null) { + insertDirDeleteRecordInDB(directory) + } else if (folder.isEnabled) { + folder.setEnabled(false) + DbHelper.updateSyncedFolder(folder, context) + } + } + + private fun insertDirDeleteRecordInDB(directory: File) { + //look for parent + val parentFile = directory.parentFile ?: return + + val parentPath = getLocalPath(parentFile) + val parentFolder = DbHelper.getSyncedFolderByLocalPath(parentPath, context) ?: return + + val folder = SyncedFolder( + parentFolder, + directory.name + PATH_SEPARATOR, + directory.lastModified(), + "" + ) + folder.setEnabled(false) + DbHelper.insertSyncedFolder(folder, context) + } + + /** + * handle a file close_write event for a file which is not a directory + * @param file File that has been modified + */ + private fun handleFileCloseWrite(file: File) { + val fileLocalPath = getLocalPath(file) + Timber.d("handleFileCloseWrite( $fileLocalPath )") + var request: SyncRequest? = null + val fileState = DbHelper.loadSyncedFile(context, fileLocalPath, true) + + if (fileState == null) { //New file discovered + request = handleNewFileCreation(file) + } else { //File update + val isWaitingForDownload = fileState.isLastEtagStored() && fileState.lastModified == 0L + if (fileState.scanScope > SCAN_ON_CLOUD && !isWaitingForDownload) { + request = SyncRequest(fileState, SyncRequest.Type.UPLOAD) + } + } + + request?.let { + sendSyncRequestToSynchronizationService(it) + } + } + + private fun handleFileMoveTo(file: File) { + val fileLocalPath = getLocalPath(file) + Timber.d("handleFileMoveTo( $fileLocalPath )") + var request: SyncRequest? = null + val fileState = DbHelper.loadSyncedFile(context, fileLocalPath, true) + + if (fileState == null) { //New file discovered + request = handleNewFileCreation(file) + } + + request?.let { + sendSyncRequestToSynchronizationService(it) + } + } + + private fun handleNewFileCreation(file: File): SyncRequest? { + val parentFile = file.parentFile ?: return null + + val parentPath = getLocalPath(parentFile) + val parentFolder = DbHelper.getSyncedFolderByLocalPath(parentPath, context) + if (parentFolder == null || !parentFolder.isEnabled) { + Timber.d("Won't send sync request: no parent are known for new file: %s", file.name) + return null + } + + var scanScope = DO_NOT_SCAN + if (parentFolder.isEnabled) { + if (parentFolder.isScanRemote) scanScope = SCAN_ON_CLOUD + if (parentFolder.isScanLocal) scanScope += SCAN_ON_DEVICE + } + + val remotePath = parentFolder.remoteFolder + file.name + val fileState = SyncedFileState( + -1, file.name, getLocalPath(file), remotePath, "", 0L, + parentFolder.id.toLong(), parentFolder.isMediaType, scanScope + ) + return insertNewFileStateIntoDB(fileState, file) + } + + private fun insertNewFileStateIntoDB( + fileState: SyncedFileState, + file: File + ): SyncRequest? { + val storedId = DbHelper.manageSyncedFileStateDB(fileState, "INSERT", context) + if (storedId > 0) { + fileState.id = storedId + return SyncRequest(fileState, SyncRequest.Type.UPLOAD) + } + + Timber.d("New File ${file.name} observed but impossible to insert it in DB") + return null + } + + /** + * Handle a file deletion event for a file which is not a directory + * @param file File that has been removed + */ + private fun handleFileDelete(file: File) { + val fileLocalPath = getLocalPath(file) + Timber.d("handleFileDelete( $fileLocalPath )") + val fileState = DbHelper.loadSyncedFile(context, fileLocalPath, true) ?: return + + //If already in DB + if (fileState.scanScope > DO_NOT_SCAN) { + //todo: if file is already sync disabled, we should probably remove file from DB + val disableSyncingRequest = SyncRequest(fileState, SyncRequest.Type.DISABLE_SYNCING) + sendSyncRequestToSynchronizationService(disableSyncingRequest) + } + } +} diff --git a/app/src/main/java/foundation/e/drive/fileObservers/FileEventListener.kt b/app/src/main/java/foundation/e/drive/fileObservers/FileEventListener.kt new file mode 100644 index 0000000000000000000000000000000000000000..f75a95f780026bdf591247da065d604a3dbc9670 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/fileObservers/FileEventListener.kt @@ -0,0 +1,15 @@ +/* + * 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 java.io.File + +interface FileEventListener { + fun notify(event: Int, file: File, dirPath: String) +} diff --git a/app/src/main/java/foundation/e/drive/fileObservers/FileObserverManager.kt b/app/src/main/java/foundation/e/drive/fileObservers/FileObserverManager.kt new file mode 100644 index 0000000000000000000000000000000000000000..39bb91fa1f22afda9336b5ea084159e3fdf15d22 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/fileObservers/FileObserverManager.kt @@ -0,0 +1,144 @@ +/* + * Copyright © MURENA SAS 2023. + * 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.Build +import android.os.FileObserver +import foundation.e.drive.database.DbHelper +import foundation.e.drive.models.SyncedFolder +import timber.log.Timber +import java.io.File +import java.io.FileFilter +import java.util.Stack + +/** + * @author Narinder Rana + * @author Vincent Bourgmayer + * @author Fahim Salam Chowdhury + */ +class FileObserverManager(private val context: Context) : FileEventListener { + + companion object { + val WATCHABLE_DIRECTORIES_FILTER = FileFilter { + it.isDirectory && !it.isHidden + } + } + + private val observers = HashMap() + private val handler = FileEventHandler(context) + + private var watching = false + + override fun notify(event: Int, file: File, dirPath: String) { + if (file.isHidden) { + return + } + + Timber.d("notified for the file ${file.absolutePath} with event: $event dirPath: $dirPath") + + when (event) { + FileObserver.DELETE_SELF -> stopWatching(dirPath) + FileObserver.CREATE -> startWatchingDirectory(file) + } + + handler.onEvent(event, file) + } + + fun initializeObserving() { + if (watching) { + return + } + + stopObserving() + + val syncedFolders = DbHelper.getAllSyncedFolders(context) + if (syncedFolders.isEmpty()) { + return + } + + val stack = Stack() + syncedFolders.map(SyncedFolder::getLocalFolder) + .forEach(stack::push) + + watching = true + + // Recursively watch all child directories + recursivelyWatchAllDirectories(stack) + + Timber.d("started observing all syncable directories") + } + + private fun recursivelyWatchAllDirectories(stack: Stack) { + while (!stack.empty()) { + val parent = stack.pop() + startWatching(parent) + + val path = File(parent) + val files = path.listFiles(WATCHABLE_DIRECTORIES_FILTER) ?: continue + + files.map(File::getAbsolutePath) + .forEach(stack::push) + } + } + + private fun startWatchingDirectory(file: File) { + if (WATCHABLE_DIRECTORIES_FILTER.accept(file)) { + startWatching(file.absolutePath) + } + } + + /** + * Start watching a single file + */ + private fun startWatching(path: String) { + synchronized(observers) { + stopObservingFile(path) + startObservingFile(path) + } + } + + private fun stopObservingFile(path: String) { + observers.remove(path)?.let { + it.stopWatching() + Timber.d("stop observing file: $path") + } + } + + private fun startObservingFile(path: String) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + return + } + + val observer = DirectoryObserver(path, this) + observer.startWatching() + observers[path] = observer + Timber.d("start observing file: $path") + } + + fun stopObserving() { + observers.forEach { + it.value.stopWatching() + } + observers.clear() + watching = false + + Timber.d("stopped observing all syncable directories") + } + + /** + * Stop watching a single file + */ + private fun stopWatching(path: String) { + synchronized(observers) { + stopObservingFile(path) + } + } +} diff --git a/app/src/main/java/foundation/e/drive/synchronization/SyncProxy.kt b/app/src/main/java/foundation/e/drive/synchronization/SyncProxy.kt index bd2df4301e71e6c5e33e009fd258e18e41442893..dbd8f5f69365c9770121f2f5139a147a85e48e89 100644 --- a/app/src/main/java/foundation/e/drive/synchronization/SyncProxy.kt +++ b/app/src/main/java/foundation/e/drive/synchronization/SyncProxy.kt @@ -119,7 +119,7 @@ object SyncProxy: SyncRequestCollector, SyncManager { if (syncRequestQueue.isEmpty()) { Timber.d("Request queue is empty") - StateMachine.changeState(SyncState.LISTENING_FILES) + startListeningFiles(context) return } @@ -128,9 +128,7 @@ object SyncProxy: SyncRequestCollector, SyncManager { if (!isStateChanged) return - if (previousSyncState == SyncState.PERIODIC_SCAN) { - context.startRecursiveFileObserver() - } + context.startRecursiveFileObserver() if (previousSyncState != SyncState.SYNCHRONIZING) { WorkerUtils.enqueueOneTimeSync(WorkManager.getInstance(context)) @@ -151,8 +149,6 @@ object SyncProxy: SyncRequestCollector, SyncManager { val isStateChanged = StateMachine.changeState(SyncState.PERIODIC_SCAN) if (!isStateChanged) return false - application.stopRecursiveFileObserver() - return true } @@ -167,14 +163,8 @@ object SyncProxy: SyncRequestCollector, SyncManager { return } - val previousSyncState = StateMachine.currentState - val isStateChanged = StateMachine.changeState(SyncState.LISTENING_FILES) - - if (!isStateChanged) return - - if (previousSyncState == SyncState.IDLE || previousSyncState == SyncState.PERIODIC_SCAN) { - application.startRecursiveFileObserver() - } + StateMachine.changeState(SyncState.LISTENING_FILES) + application.startRecursiveFileObserver() }