Loading README.md +55 −4 Original line number Diff line number Diff line Loading @@ -49,7 +49,7 @@ eDrive can also receive some broadcast intent for different purpose: **Force the synchronization** ```bash adb shell am broadcast -a foundation.e.drive.action.FORCE_SYNC --receiver-include-background adb shell am broadcast -a foundation.e.drive.action.FORCE_SCAN --receiver-include-background ``` **Generate a database dump accessible by the user** Loading @@ -58,6 +58,12 @@ adb shell am broadcast -a foundation.e.drive.action.FORCE_SYNC --receiver-includ adb shell am broadcast -a foundation.e.drive.action.DUMP_DATABASE --receiver-include-background ``` or Download database directly: ```bash adb pull /data/data/foundation.e.drive/databases ``` **Disable log limit on release build** ```bash Loading @@ -70,4 +76,49 @@ adb shell am broadcast -a foundation.e.drive.action.DUMP_DATABASE --receiver-inc adb shell am broadcast -a foundation.e.drive.action.FULL_LOG_ON_PROD --receiver-include-background --ez full_log_enable false ``` [doc-persistence]: https://developer.android.com/guide/topics/manifest/application-element#persistent No newline at end of file **Test SyncWorker: upload 10 empty files** Does not rely on FileObserver or FullScanWorker ```bash adb shell am broadcast -a foundation.e.drive.action.TEST_SYNC --receiver-include-background ``` **Display logat** ```bash adb logcat --pid=$(adb shell pidof -s foundation.e.drive) ``` You can also use the script `dev-tools.sh` to run those command. Use : `./dev-tools.sh -h` to display options **Disable File Observer** ```bash adb shell am broadcast -a foundation.e.drive.action.ENABLE_FILE_OBSERVER --receiver-include-background --ez file_observer_enable false ``` **Enable File Observer** ```bash adb shell am broadcast -a foundation.e.drive.action.ENABLE_FILE_OBSERVER --receiver-include-background --ez file_observer_enable true ``` ### local NC for testing Use following documentation to set up a local NC instance https://gitlab.e.foundation/internal/wiki/-/wikis/qa-team/how-to-run-local-nextcloud `docker run -d -p 8080:80 -e NEXTCLOUD_TRUSTED_DOMAINS="<your computer IP>:8080" -e SQLITE_DATABASE=nc -e NEXTCLOUD_ADMIN_USER=admin -e NEXTCLOUD_ADMIN_PASSWORD=admin nextcloud:<NC version> ` Then you can use Wireshark by example to collect network packet. Example of filter for wireshark: `(ip.src == <computer IP> && ip.dst == <Phone IP>) || (ip.dst == <computer IP> && ip.src == <Phone IP> ) && http` *note: replace <computer IP>, <NC version> and <Phone IP> by values* app/build.gradle +8 −10 Original line number Diff line number Diff line Loading @@ -3,10 +3,9 @@ plugins { id 'org.jetbrains.kotlin.android' } def versionMajor = 1 def versionMinor = 4 def versionPatch = 4 def versionMinor = 5 def versionPatch = 1 def getTestProp(String propName) { def result = "" Loading Loading @@ -101,16 +100,15 @@ dependencies { implementation 'foundation.e:Nextcloud-Android-Library:1.0.8-u2.17-release' implementation "commons-httpclient:commons-httpclient:3.1@jar" implementation fileTree(include: ['*.jar'], dir: 'libs') api 'androidx.annotation:annotation:1.6.0' implementation 'androidx.core:core:1.10.1' api 'androidx.annotation:annotation:1.7.0' implementation 'androidx.appcompat:appcompat:1.6.1' implementation "androidx.constraintlayout:constraintlayout:2.1.4" implementation 'com.google.android.material:material:1.9.0' implementation 'com.google.android.material:material:1.10.0' implementation 'com.github.bumptech.glide:glide:4.15.1' implementation 'com.github.bumptech.glide:annotations:4.15.1' implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.core:core-ktx:1.12.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.15.1' implementation "androidx.work:work-runtime:2.8.1" implementation "androidx.work:work-runtime:2.9.0" implementation 'androidx.test:core:1.5.0' implementation 'com.jakewharton.timber:timber:5.0.1' implementation 'foundation.e:elib:0.0.1-alpha11' Loading @@ -118,7 +116,7 @@ dependencies { androidTestImplementation 'androidx.test:runner:1.5.2' androidTestImplementation 'androidx.test:rules:1.5.0' androidTestImplementation 'androidx.annotation:annotation:1.6.0' androidTestImplementation 'androidx.annotation:annotation:1.7.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation 'junit:junit:4.13.2' Loading @@ -128,5 +126,5 @@ dependencies { testImplementation 'junit:junit:4.13.2' testImplementation 'org.robolectric:robolectric:4.7.3' testImplementation 'org.mockito:mockito-core:5.0.0' testImplementation 'androidx.work:work-testing:2.8.1' testImplementation 'androidx.work:work-testing:2.9.0' } app/src/main/AndroidManifest.xml +9 −3 Original line number Diff line number Diff line Loading @@ -12,10 +12,15 @@ <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" /> <!-- for Android 30+ --> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <!-- needed for PersistedJob --> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.GET_ACCOUNTS" /> <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" /> <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/> <uses-permission android:name="android.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND" tools:ignore="ProtectedPermissions" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/> <uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" /> <uses-permission android:name="foundation.e.pwaplayer.provider.READ_WRITE" /> <uses-permission Loading @@ -37,7 +42,6 @@ android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:persistent="true" android:roundIcon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true"> <!-- required to access media folder since app target sdk is 29+ but deprecated. Remove when Q support will be dropped--> <activity Loading Loading @@ -98,9 +102,11 @@ android:exported="true" tools:ignore="ExportedReceiver"> <intent-filter> <action android:name="foundation.e.drive.action.FORCE_SYNC" /> <action android:name="foundation.e.drive.action.FORCE_SCAN" /> <action android:name="foundation.e.drive.action.DUMP_DATABASE"/> <action android:name="foundation.e.drive.action.FULL_LOG_ON_PROD"/> <action android:name="foundation.e.drive.action.ENABLE_FILE_OBSERVER"/> <action android:name="foundation.e.drive.action.TEST_SYNC"/> </intent-filter> </receiver> Loading app/src/main/java/foundation/e/drive/EdriveApplication.java +19 −30 Original line number Diff line number Diff line Loading @@ -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()); Loading @@ -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) Loading @@ -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 Loading @@ -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; Loading app/src/main/java/foundation/e/drive/FileObservers/FileEventListener.javadeleted 100644 → 0 +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); } } } Loading
README.md +55 −4 Original line number Diff line number Diff line Loading @@ -49,7 +49,7 @@ eDrive can also receive some broadcast intent for different purpose: **Force the synchronization** ```bash adb shell am broadcast -a foundation.e.drive.action.FORCE_SYNC --receiver-include-background adb shell am broadcast -a foundation.e.drive.action.FORCE_SCAN --receiver-include-background ``` **Generate a database dump accessible by the user** Loading @@ -58,6 +58,12 @@ adb shell am broadcast -a foundation.e.drive.action.FORCE_SYNC --receiver-includ adb shell am broadcast -a foundation.e.drive.action.DUMP_DATABASE --receiver-include-background ``` or Download database directly: ```bash adb pull /data/data/foundation.e.drive/databases ``` **Disable log limit on release build** ```bash Loading @@ -70,4 +76,49 @@ adb shell am broadcast -a foundation.e.drive.action.DUMP_DATABASE --receiver-inc adb shell am broadcast -a foundation.e.drive.action.FULL_LOG_ON_PROD --receiver-include-background --ez full_log_enable false ``` [doc-persistence]: https://developer.android.com/guide/topics/manifest/application-element#persistent No newline at end of file **Test SyncWorker: upload 10 empty files** Does not rely on FileObserver or FullScanWorker ```bash adb shell am broadcast -a foundation.e.drive.action.TEST_SYNC --receiver-include-background ``` **Display logat** ```bash adb logcat --pid=$(adb shell pidof -s foundation.e.drive) ``` You can also use the script `dev-tools.sh` to run those command. Use : `./dev-tools.sh -h` to display options **Disable File Observer** ```bash adb shell am broadcast -a foundation.e.drive.action.ENABLE_FILE_OBSERVER --receiver-include-background --ez file_observer_enable false ``` **Enable File Observer** ```bash adb shell am broadcast -a foundation.e.drive.action.ENABLE_FILE_OBSERVER --receiver-include-background --ez file_observer_enable true ``` ### local NC for testing Use following documentation to set up a local NC instance https://gitlab.e.foundation/internal/wiki/-/wikis/qa-team/how-to-run-local-nextcloud `docker run -d -p 8080:80 -e NEXTCLOUD_TRUSTED_DOMAINS="<your computer IP>:8080" -e SQLITE_DATABASE=nc -e NEXTCLOUD_ADMIN_USER=admin -e NEXTCLOUD_ADMIN_PASSWORD=admin nextcloud:<NC version> ` Then you can use Wireshark by example to collect network packet. Example of filter for wireshark: `(ip.src == <computer IP> && ip.dst == <Phone IP>) || (ip.dst == <computer IP> && ip.src == <Phone IP> ) && http` *note: replace <computer IP>, <NC version> and <Phone IP> by values*
app/build.gradle +8 −10 Original line number Diff line number Diff line Loading @@ -3,10 +3,9 @@ plugins { id 'org.jetbrains.kotlin.android' } def versionMajor = 1 def versionMinor = 4 def versionPatch = 4 def versionMinor = 5 def versionPatch = 1 def getTestProp(String propName) { def result = "" Loading Loading @@ -101,16 +100,15 @@ dependencies { implementation 'foundation.e:Nextcloud-Android-Library:1.0.8-u2.17-release' implementation "commons-httpclient:commons-httpclient:3.1@jar" implementation fileTree(include: ['*.jar'], dir: 'libs') api 'androidx.annotation:annotation:1.6.0' implementation 'androidx.core:core:1.10.1' api 'androidx.annotation:annotation:1.7.0' implementation 'androidx.appcompat:appcompat:1.6.1' implementation "androidx.constraintlayout:constraintlayout:2.1.4" implementation 'com.google.android.material:material:1.9.0' implementation 'com.google.android.material:material:1.10.0' implementation 'com.github.bumptech.glide:glide:4.15.1' implementation 'com.github.bumptech.glide:annotations:4.15.1' implementation 'androidx.core:core-ktx:1.10.1' implementation 'androidx.core:core-ktx:1.12.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.15.1' implementation "androidx.work:work-runtime:2.8.1" implementation "androidx.work:work-runtime:2.9.0" implementation 'androidx.test:core:1.5.0' implementation 'com.jakewharton.timber:timber:5.0.1' implementation 'foundation.e:elib:0.0.1-alpha11' Loading @@ -118,7 +116,7 @@ dependencies { androidTestImplementation 'androidx.test:runner:1.5.2' androidTestImplementation 'androidx.test:rules:1.5.0' androidTestImplementation 'androidx.annotation:annotation:1.6.0' androidTestImplementation 'androidx.annotation:annotation:1.7.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation 'junit:junit:4.13.2' Loading @@ -128,5 +126,5 @@ dependencies { testImplementation 'junit:junit:4.13.2' testImplementation 'org.robolectric:robolectric:4.7.3' testImplementation 'org.mockito:mockito-core:5.0.0' testImplementation 'androidx.work:work-testing:2.8.1' testImplementation 'androidx.work:work-testing:2.9.0' }
app/src/main/AndroidManifest.xml +9 −3 Original line number Diff line number Diff line Loading @@ -12,10 +12,15 @@ <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" /> <!-- for Android 30+ --> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <!-- needed for PersistedJob --> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.GET_ACCOUNTS" /> <uses-permission android:name="android.permission.READ_SYNC_SETTINGS" /> <uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/> <uses-permission android:name="android.permission.START_FOREGROUND_SERVICES_FROM_BACKGROUND" tools:ignore="ProtectedPermissions" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/> <uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" /> <uses-permission android:name="foundation.e.pwaplayer.provider.READ_WRITE" /> <uses-permission Loading @@ -37,7 +42,6 @@ android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:persistent="true" android:roundIcon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true"> <!-- required to access media folder since app target sdk is 29+ but deprecated. Remove when Q support will be dropped--> <activity Loading Loading @@ -98,9 +102,11 @@ android:exported="true" tools:ignore="ExportedReceiver"> <intent-filter> <action android:name="foundation.e.drive.action.FORCE_SYNC" /> <action android:name="foundation.e.drive.action.FORCE_SCAN" /> <action android:name="foundation.e.drive.action.DUMP_DATABASE"/> <action android:name="foundation.e.drive.action.FULL_LOG_ON_PROD"/> <action android:name="foundation.e.drive.action.ENABLE_FILE_OBSERVER"/> <action android:name="foundation.e.drive.action.TEST_SYNC"/> </intent-filter> </receiver> Loading
app/src/main/java/foundation/e/drive/EdriveApplication.java +19 −30 Original line number Diff line number Diff line Loading @@ -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()); Loading @@ -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) Loading @@ -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 Loading @@ -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; Loading
app/src/main/java/foundation/e/drive/FileObservers/FileEventListener.javadeleted 100644 → 0 +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); } } }