diff --git a/README.md b/README.md index bd9d9ea6cb98db1f103993cb99071a734d410746..35f46a7fcbe52c49429e081635346258a4d9bc04 100644 --- a/README.md +++ b/README.md @@ -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** @@ -58,16 +58,67 @@ 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 - adb shell am broadcast -a foundation.e.drive.action.FULL_LOG_ON_PROD --receiver-include-background --ez full_log_enable true +adb shell am broadcast -a foundation.e.drive.action.FULL_LOG_ON_PROD --receiver-include-background --ez full_log_enable true ``` **Limit log output on release build** *(after previous command)* ```bash - adb shell am broadcast -a foundation.e.drive.action.FULL_LOG_ON_PROD --receiver-include-background --ez full_log_enable false +adb shell am broadcast -a foundation.e.drive.action.FULL_LOG_ON_PROD --receiver-include-background --ez full_log_enable false +``` + +**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) ``` -[doc-persistence]: https://developer.android.com/guide/topics/manifest/application-element#persistent \ No newline at end of file +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=":8080" -e SQLITE_DATABASE=nc -e NEXTCLOUD_ADMIN_USER=admin -e NEXTCLOUD_ADMIN_PASSWORD=admin nextcloud: +` + +Then you can use Wireshark by example to collect network packet. +Example of filter for wireshark: +`(ip.src == && ip.dst == ) || (ip.dst == && ip.src == ) && http` + + +*note: replace , and by values* diff --git a/app/build.gradle b/app/build.gradle index c8784861a629f6acb55d0ea475181b6d9f476e16..ab43e1c46108aebdce876c00413503c864740db8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 = "" @@ -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' @@ -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' @@ -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' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d8017097a04a82dac6b15ffbc4769678b37678a5..3bf33e1df19f5570fad891c4e22275fa81f6773e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,10 +12,15 @@ - + + + + + - + + + 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/account/AccountUtils.kt b/app/src/main/java/foundation/e/drive/account/AccountUtils.kt new file mode 100644 index 0000000000000000000000000000000000000000..ae3c1118b1f341e911c73d63aa48e4d2615b74d9 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/account/AccountUtils.kt @@ -0,0 +1,79 @@ +/* + * Copyright MURENA SAS 2022-2024 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.drive.account + +import android.accounts.Account +import android.accounts.AccountManager +import android.content.Context +import foundation.e.drive.R +import foundation.e.drive.utils.AppConstants.ACCOUNT_DATA_GROUPS +import foundation.e.drive.utils.AppConstants.SHARED_PREFERENCE_NAME + +object AccountUtils { + + @JvmStatic + fun getPremiumPlan(accountManager: AccountManager, account: Account?): String? { + if (account == null) return null + val groupData = accountManager.getUserData(account, ACCOUNT_DATA_GROUPS) + val premiumGroup = extractPremiumGroup(groupData) + return extractPremiumPlan(premiumGroup) + } + + @JvmStatic + private fun extractPremiumPlan(premiumGroup: String?): String? { + if (premiumGroup.isNullOrEmpty()) return null + + val splitPremiumGroup = premiumGroup.split("-") + return if (splitPremiumGroup.size < 2) null + else splitPremiumGroup[1] + } + + @JvmStatic + private fun extractPremiumGroup(groupData: String?): String? { + if (groupData.isNullOrEmpty()) return null + + val groups = groupData.split(",") + return groups.firstOrNull { group: String -> group.contains("premium-") } + } + + @JvmStatic + fun getAccount(context: Context): Account? { + val prefs = context.getSharedPreferences(SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE) + val accountName = prefs.getString(AccountManager.KEY_ACCOUNT_NAME, "") + + return getAccount(accountName!!, context) + } + + @JvmStatic + fun getAccount(accountName: String, context: Context): Account? { + if (accountName.isEmpty()) return null + + val accountManager = AccountManager.get(context) + val accountType = context.getString(R.string.eelo_account_type) + + return accountManager.getAccountsByType(accountType) + .firstOrNull { account -> account.name == accountName } + } + + @JvmStatic + fun isAccountAvailable(context: Context): Boolean { + val accountManager = AccountManager.get(context.applicationContext) + val accountList = + accountManager.getAccountsByType(context.getString(R.string.eelo_account_type)) + return accountList.isNotEmpty() + } +} diff --git a/app/src/main/java/foundation/e/drive/account/receivers/AccountAddedReceiver.kt b/app/src/main/java/foundation/e/drive/account/receivers/AccountAddedReceiver.kt index 0459bf3b9680349cd530509b6a8501c95114de06..b2c51e5b84ea344b3570eb9140cf1692c7fd282f 100644 --- a/app/src/main/java/foundation/e/drive/account/receivers/AccountAddedReceiver.kt +++ b/app/src/main/java/foundation/e/drive/account/receivers/AccountAddedReceiver.kt @@ -1,5 +1,5 @@ /* - * Copyright © MURENA SAS 2023. + * Copyright © MURENA SAS 2023-2024. * 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 @@ -13,11 +13,10 @@ import android.content.Context import android.content.Intent import android.content.SharedPreferences import foundation.e.drive.R -import foundation.e.drive.utils.AccountUtils +import foundation.e.drive.account.AccountUtils import foundation.e.drive.utils.AppConstants -import foundation.e.drive.utils.CommonUtils import foundation.e.drive.utils.DavClientProvider -import foundation.e.drive.utils.WorkerUtils +import foundation.e.drive.work.WorkLauncher import timber.log.Timber /** @@ -45,8 +44,11 @@ class AccountAddedReceiver() : BroadcastReceiver() { .putString(AccountManager.KEY_ACCOUNT_NAME, accountName) .apply() - DavClientProvider.getInstance().cleanUp(); - WorkerUtils.registerSetupWorkers(context) + val workLauncher = WorkLauncher.getInstance(context) + if (workLauncher.enqueueSetupWorkers(context)) { + DavClientProvider.getInstance().cleanUp() + workLauncher.enqueuePeriodicUserInfoFetching() + } } /** @@ -62,7 +64,7 @@ class AccountAddedReceiver() : BroadcastReceiver() { prefs: SharedPreferences, context: Context ): Boolean { - if (AccountUtils.isSetupAlreadyDone(prefs)) { + if (isSetupAlreadyDone(prefs)) { return false } @@ -74,8 +76,8 @@ class AccountAddedReceiver() : BroadcastReceiver() { return false } - if (!isExistingAccount(accountName, accountType, context)) { - Timber.w("No account exist for type: %s, username: %s", accountType, accountName) + if (!isExistingAccount(accountName, context)) { + Timber.w("No account exist for username: %s ", accountType, accountName) return false } return true @@ -86,9 +88,11 @@ class AccountAddedReceiver() : BroadcastReceiver() { return accountType != validAccountType } - private fun isExistingAccount(accountName: String, accountType: String, context: Context): Boolean { - val account = CommonUtils.getAccount(accountName, accountType, AccountManager.get(context)) - return account != null + private fun isSetupAlreadyDone(prefs: SharedPreferences): Boolean { + return prefs.getBoolean(AppConstants.SETUP_COMPLETED, false) } -} \ No newline at end of file + private fun isExistingAccount(accountName: String, context: Context): Boolean { + return AccountUtils.getAccount(accountName, context) != null + } +} diff --git a/app/src/main/java/foundation/e/drive/account/setup/FinishSetupWorker.java b/app/src/main/java/foundation/e/drive/account/setup/FinishSetupWorker.java index 316f58abe3f4a52776cc408951e3371afe9a75de..f1ed29853751feb672a22c6f1abde74ac3799fe0 100644 --- a/app/src/main/java/foundation/e/drive/account/setup/FinishSetupWorker.java +++ b/app/src/main/java/foundation/e/drive/account/setup/FinishSetupWorker.java @@ -11,24 +11,15 @@ package foundation.e.drive.account.setup; import static android.content.Context.MODE_PRIVATE; import static foundation.e.drive.utils.AppConstants.INITIAL_FOLDER_NUMBER; import static foundation.e.drive.utils.AppConstants.SHARED_PREFERENCE_NAME; -import static foundation.e.drive.work.WorkRequestFactory.WorkType.ONE_TIME_APP_LIST; -import static foundation.e.drive.work.WorkRequestFactory.WorkType.ONE_TIME_FORCED_FULL_SCAN; -import static foundation.e.drive.work.WorkRequestFactory.WorkType.PERIODIC_SCAN; import android.content.Context; import androidx.annotation.NonNull; -import androidx.work.ExistingPeriodicWorkPolicy; -import androidx.work.ExistingWorkPolicy; -import androidx.work.WorkManager; import androidx.work.Worker; import androidx.work.WorkerParameters; -import foundation.e.drive.periodicScan.FullScanWorker; -import foundation.e.drive.periodicScan.PeriodicScanWorker; import foundation.e.drive.utils.AppConstants; -import foundation.e.drive.utils.CommonUtils; -import foundation.e.drive.work.WorkRequestFactory; +import foundation.e.drive.work.WorkLauncher; import timber.log.Timber; /** @@ -68,29 +59,9 @@ public class FinishSetupWorker extends Worker { } private void enqueueWorkers(@NonNull final Context appContext) { - final WorkManager workManager = WorkManager.getInstance(appContext); - enqueueAppListGenerationWorker(workManager); - enqueueFullScanWorker(workManager); - enqueuePeriodicFileScanWorker(workManager); - - CommonUtils.registerPeriodicUserInfoChecking(workManager); - } - - private void enqueueFullScanWorker(@NonNull final WorkManager workManager) { - workManager.enqueueUniqueWork( - FullScanWorker.UNIQUE_WORK_NAME, - ExistingWorkPolicy.KEEP, - WorkRequestFactory.getOneTimeWorkRequest(ONE_TIME_FORCED_FULL_SCAN, null) - ); - } - - private void enqueueAppListGenerationWorker(@NonNull final WorkManager workManager) { - workManager.enqueue(WorkRequestFactory.getOneTimeWorkRequest(ONE_TIME_APP_LIST, null)); - } - - private void enqueuePeriodicFileScanWorker(@NonNull final WorkManager workManager) { - workManager.enqueueUniquePeriodicWork(PeriodicScanWorker.UNIQUE_WORK_NAME, - ExistingPeriodicWorkPolicy.KEEP, - WorkRequestFactory.getPeriodicWorkRequest(PERIODIC_SCAN)); + final WorkLauncher launcher = WorkLauncher.getInstance(appContext); + launcher.enqueueOneTimeAppListGenerator(); + launcher.enqueueOneTimeFullScan(false); + launcher.enqueuePeriodicFullScan(); } } \ No newline at end of file diff --git a/app/src/main/java/foundation/e/drive/account/setup/RootFolderSetupWorker.java b/app/src/main/java/foundation/e/drive/account/setup/RootFolderSetupWorker.java index b028653c990dbf6937b3e2f9fb0ce55f37b2ac37..d6780a2c0ba1649edd84a30f6b24167788861dba 100644 --- a/app/src/main/java/foundation/e/drive/account/setup/RootFolderSetupWorker.java +++ b/app/src/main/java/foundation/e/drive/account/setup/RootFolderSetupWorker.java @@ -9,9 +9,7 @@ package foundation.e.drive.account.setup; import android.accounts.Account; -import android.accounts.AccountManager; import android.content.Context; -import android.content.SharedPreferences; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -25,10 +23,9 @@ import com.owncloud.android.lib.resources.files.CreateFolderRemoteOperation; import java.io.File; -import foundation.e.drive.R; +import foundation.e.drive.account.AccountUtils; import foundation.e.drive.database.DbHelper; import foundation.e.drive.models.SyncedFolder; -import foundation.e.drive.utils.AppConstants; import foundation.e.drive.utils.CommonUtils; import foundation.e.drive.utils.DavClientProvider; import timber.log.Timber; @@ -62,7 +59,7 @@ public class RootFolderSetupWorker extends Worker { public Result doWork() { try { final Context context = getApplicationContext(); - final Account account = getAccount(); + final Account account = AccountUtils.getAccount(getApplicationContext()); if (account == null) { Timber.d("doWork(): Can't get valid account"); return Result.failure(); @@ -132,14 +129,4 @@ public class RootFolderSetupWorker extends Worker { return result; } - - @Nullable - private Account getAccount() { - final SharedPreferences prefs = getApplicationContext().getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE); - final String accountName = prefs.getString(AccountManager.KEY_ACCOUNT_NAME, ""); - final String accountType = getApplicationContext().getString(R.string.eelo_account_type); - - if (accountName.isEmpty()) return null; - return CommonUtils.getAccount(accountName, accountType, AccountManager.get(getApplicationContext())); - } } diff --git a/app/src/main/java/foundation/e/drive/activity/AccountsActivity.java b/app/src/main/java/foundation/e/drive/activity/AccountsActivity.java index b4e0ad42f96473af51164c758bc480559ed9cb63..b995fcad0f5c36ebdc3218018f1f1238f8b036f1 100644 --- a/app/src/main/java/foundation/e/drive/activity/AccountsActivity.java +++ b/app/src/main/java/foundation/e/drive/activity/AccountsActivity.java @@ -33,8 +33,8 @@ import com.bumptech.glide.Glide; import com.owncloud.android.lib.common.OwnCloudClient; import foundation.e.drive.R; +import foundation.e.drive.account.AccountUtils; import foundation.e.drive.databinding.ActivityAccountsBinding; -import foundation.e.drive.utils.AccountUtils; import foundation.e.drive.utils.CommonUtils; import foundation.e.drive.utils.DavClientProvider; import foundation.e.drive.utils.AccessTokenProvider; @@ -115,9 +115,10 @@ public class AccountsActivity extends AppCompatActivity { binding.plan.setText(getString(R.string.free_plan, totalShownQuota)); - AccountUtils.getPremiumGroup(accountManager, account) - .ifPresent(group -> binding.plan.setText(getString(R.string.premium_plan, - group.split("-")[1]))); + final String premiumPlan = AccountUtils.getPremiumPlan(accountManager, account); + if (premiumPlan != null) { + binding.plan.setText(getString(R.string.premium_plan, premiumPlan)); + } binding.myPlan.setVisibility(View.VISIBLE); binding.plan.setVisibility(View.VISIBLE); diff --git a/app/src/main/java/foundation/e/drive/database/DbHelper.java b/app/src/main/java/foundation/e/drive/database/DbHelper.java index a71ca586d39877c9fc988688d0465bdbe8ad5498..5a4ac077c73a99ca5bee72b714824e90ba6b850c 100644 --- a/app/src/main/java/foundation/e/drive/database/DbHelper.java +++ b/app/src/main/java/foundation/e/drive/database/DbHelper.java @@ -47,7 +47,6 @@ public final class DbHelper extends SQLiteOpenHelper { */ public DbHelper(@NonNull Context context){ super(context, DATABASE_NAME, null, DATABASE_VERSION); - Timber.tag(DbHelper.class.getSimpleName()); } /** diff --git a/app/src/main/java/foundation/e/drive/fileFilters/AppSettingsFileFilter.java b/app/src/main/java/foundation/e/drive/fileFilters/AppSettingsFileFilter.java deleted file mode 100644 index 956efe440c761f356c461467991fb43ffc20990c..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/drive/fileFilters/AppSettingsFileFilter.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright © CLEUS SAS 2018-2019. - * Copyright © ECORP SAS 2020. - * 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.fileFilters; - -import java.io.File; -import java.io.FileFilter; -import java.util.Locale; - -/** - * @author Vincent Bourgmayer - * FileFilter for application Settings - */ -class AppSettingsFileFilter implements FileFilter { - @Override - public boolean accept(File pathname) { - return pathname.isFile() - || (pathname.isDirectory() - && !pathname.getName().toLowerCase(Locale.ROOT).contains("cache")); - } -} diff --git a/app/src/main/java/foundation/e/drive/fileFilters/CrashlogsFileFilter.java b/app/src/main/java/foundation/e/drive/fileFilters/CrashlogsFileFilter.java deleted file mode 100644 index 6f28fddc676678d23811123d12a6ec5d26875f82..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/drive/fileFilters/CrashlogsFileFilter.java +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright © CLEUS SAS 2018-2019. - * Copyright © ECORP SAS 2020. - * 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.fileFilters; - -import androidx.annotation.NonNull; - -import java.io.File; -import java.io.FileFilter; - -import foundation.e.drive.utils.ServiceExceptionHandler; - -/** - * @Author Vincent Bourgmayer - */ -public class CrashlogsFileFilter implements FileFilter { - private final static long max_timestamp_delta = 864000000; //10 days in ms (240*3600*1000) - - @Override - public boolean accept(@NonNull File pathname) { - String fileTimestamp = extractTimestamp(pathname.getName(), - ServiceExceptionHandler.LOG_FILE_NAME_PREFIX, - ServiceExceptionHandler.LOG_FILE_EXTENSION); - - long timestamp; - try { - timestamp = Long.parseLong(fileTimestamp); - }catch (NumberFormatException e){ - //Can't parse the extracted timestamp - //This file has not the expected name. It must be removed - return true; - } - - //if current Date - file date >= max deta allowed - return ((System.currentTimeMillis() - timestamp ) >= max_timestamp_delta); - } - - /** - * Extract the timestamp from the name of the file - * UnitTested! - * @param fileName Filename - * @param prefix prefix to ignore - * @param extension extension to ignore - * @return the timestamp extracted from the name - */ - private String extractTimestamp(String fileName, @NonNull String prefix, @NonNull String extension){ - return fileName.substring(prefix.length(), (fileName.length() - extension.length())); - } - -} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/drive/fileFilters/FileFilterFactory.java b/app/src/main/java/foundation/e/drive/fileFilters/FileFilterFactory.java deleted file mode 100644 index f015396523da9e6119345778fdab92d2a7cf50e2..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/drive/fileFilters/FileFilterFactory.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright © CLEUS SAS 2018-2019. - * Copyright © ECORP SAS 2020. - * 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.fileFilters; - -import androidx.annotation.NonNull; - -import java.io.FileFilter; - -/** - * @author Vincent Bourgmayer - */ -public class FileFilterFactory { - - @NonNull - public static FileFilter getFileFilter(@NonNull String category){ - FileFilter filter; - switch (category){ - case "Rom settings": - filter = new SettingsFileFilter(); - break; - case "Applications": - filter = new AppSettingsFileFilter(); - break; - case "media": - filter = new MediaFileFilter(); - break; - default: - filter = new NoCacheFileFilter(); - break; - } - return filter; - } -} diff --git a/app/src/main/java/foundation/e/drive/fileFilters/FileFilterFactory.kt b/app/src/main/java/foundation/e/drive/fileFilters/FileFilterFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..cf7fd21e46ff91d3c5dc35ebb283be7e9e904156 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/fileFilters/FileFilterFactory.kt @@ -0,0 +1,73 @@ +/* + * Copyright MURENA SAS 2024 + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package foundation.e.drive.fileFilters; + +import foundation.e.drive.utils.AppConstants +import foundation.e.drive.utils.FileUtils +import java.io.File +import java.io.FileFilter + +/** + * @author Vincent Bourgmayer + * @author Fahim Salam Chowdhury + */ +object FileFilterFactory { + + fun buildFileFilter(category: String?): FileFilter { + return when (category) { + "Rom settings" -> buildSettingsFilter() + "Applications" -> buildAppSettingsFilter() + "media" -> BasicFileFilter() + else -> buildNoCacheFileFilter() + } + } + + private fun buildSettingsFilter(): FileFilter { + return object : BasicFileFilter() { + override fun accept(pathname: File?): Boolean { + return super.accept(pathname) && pathname!!.isFile && + (pathname.name.startsWith("settings_") && pathname.name.endsWith(".xml") + || pathname.name == AppConstants.APPLICATIONS_LIST_FILE_NAME) + } + } + } + + private fun buildAppSettingsFilter(): FileFilter { + return object : BasicFileFilter() { + override fun accept(pathname: File?): Boolean { + return (super.accept(pathname) + && (pathname!!.isFile || (pathname.isDirectory + && !pathname.name.lowercase().contains("cache")))) + } + } + } + + private fun buildNoCacheFileFilter(): FileFilter { + return object : BasicFileFilter() { + override fun accept(pathname: File?): Boolean { + return super.accept(pathname) && + !pathname!!.name.lowercase().contains("cache") + } + } + } +} + +private open class BasicFileFilter : FileFilter { + override fun accept(pathname: File?): Boolean { + return pathname != null && !pathname.isHidden && FileUtils.isNotPartFile(pathname) + } +} diff --git a/app/src/main/java/foundation/e/drive/fileFilters/MediaFileFilter.java b/app/src/main/java/foundation/e/drive/fileFilters/MediaFileFilter.java deleted file mode 100644 index 6ac1740a766dc308978117983e2bfb8da96dd8cb..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/drive/fileFilters/MediaFileFilter.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright © CLEUS SAS 2018-2019. - * Copyright © ECORP SAS 2020. - * 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.fileFilters; - -import java.io.File; -import java.io.FileFilter; - -/** - * @author Vincent Bourgmayer - */ -class MediaFileFilter implements FileFilter { - /** - * Only accept not hidden files: - * Media should not be synced if they're hidden files - * @param file File to check - * @return true if file is accepted - */ - @Override - public boolean accept(File file) { - return !file.isHidden(); - } -} diff --git a/app/src/main/java/foundation/e/drive/fileFilters/NoCacheFileFilter.java b/app/src/main/java/foundation/e/drive/fileFilters/NoCacheFileFilter.java deleted file mode 100644 index 075f7d92562e9451a9e42982dfd16986e58ec4ab..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/drive/fileFilters/NoCacheFileFilter.java +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright © CLEUS SAS 2018-2019. - * 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.fileFilters; - -import java.io.File; -import java.io.FileFilter; -import java.util.Locale; - -/** - * @author Vincent Bourgmayer - */ -class NoCacheFileFilter implements FileFilter { - @Override - public boolean accept(File pathname) { - return ( ! pathname.getName().toLowerCase(Locale.ROOT).contains("cache") ); - } -} diff --git a/app/src/main/java/foundation/e/drive/fileFilters/OnlyFileFilter.java b/app/src/main/java/foundation/e/drive/fileFilters/OnlyFileFilter.java deleted file mode 100644 index a5b4b32dedeebcc9d15c22c4e4da059dfac54d99..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/drive/fileFilters/OnlyFileFilter.java +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright © CLEUS SAS 2018-2019. - * Copyright © ECORP SAS 2020. - * 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.fileFilters; - -import androidx.annotation.NonNull; - -import java.io.File; -import java.io.FileFilter; - -/** - * @author Vincent Bourgmayer - */ -public class OnlyFileFilter implements FileFilter { - @Override - public boolean accept(@NonNull File pathname) { - return (!pathname.isDirectory()); - } -} diff --git a/app/src/main/java/foundation/e/drive/fileFilters/SettingsFileFilter.java b/app/src/main/java/foundation/e/drive/fileFilters/SettingsFileFilter.java deleted file mode 100644 index 6c1420796c0580a4ba2909421c8954d054f5ce46..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/drive/fileFilters/SettingsFileFilter.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright © CLEUS SAS 2018-2019. - * Copyright © ECORP SAS 2020. - * 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.fileFilters; - -import java.io.File; -import java.io.FileFilter; -import foundation.e.drive.utils.AppConstants; - -/** - * @author Vincent Bourgmayer - * @author Narinder Rana - * Filter for Device settings synchronisation - */ - class SettingsFileFilter implements FileFilter { - /** - * Only accept file with name beginning by "settings_" and ending with ".xml". - * @param pathName path to analyze - * @return True if accepted or false - */ - @Override - public boolean accept(File pathName) { - String name = pathName.getName(); - return ( pathName.isFile() && - ( ( name.startsWith("settings_") && name.endsWith(".xml") ) - || name.equals( AppConstants.APPLICATIONS_LIST_FILE_NAME ) ) ); - } -} 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..480fc48b62ae12d80e44c3f4685b8462aee290ac --- /dev/null +++ b/app/src/main/java/foundation/e/drive/fileObservers/FileEventHandler.kt @@ -0,0 +1,249 @@ +/* + * 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 +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 (FileUtils.isPartFile(file)) { + return + } + + 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..5a7e687042fd5add6b592e808ba910176d27fb80 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/fileObservers/FileObserverManager.kt @@ -0,0 +1,145 @@ +/* + * 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 foundation.e.drive.utils.FileUtils +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 && FileUtils.isNotPartFile(it) + } + } + + 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/periodicScan/FullScanWorker.kt b/app/src/main/java/foundation/e/drive/periodicScan/FullScanWorker.kt index 08415ed91aa8c9bfddb0264109a8b0fc9a3f93dd..bd655a1c3667e63477aeb5ad3dd18613864195ed 100644 --- a/app/src/main/java/foundation/e/drive/periodicScan/FullScanWorker.kt +++ b/app/src/main/java/foundation/e/drive/periodicScan/FullScanWorker.kt @@ -8,7 +8,6 @@ package foundation.e.drive.periodicScan import android.accounts.Account -import android.accounts.AccountManager import android.app.Application import android.content.Context import android.content.SharedPreferences @@ -16,7 +15,7 @@ import androidx.work.Worker import androidx.work.WorkerParameters import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.resources.files.model.RemoteFile -import foundation.e.drive.R +import foundation.e.drive.account.AccountUtils import foundation.e.drive.database.DbHelper import foundation.e.drive.models.SyncRequest import foundation.e.drive.models.SyncedFolder @@ -51,7 +50,7 @@ class FullScanWorker(private val context: Context, private val workerParams: Wor AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE ) - val account = loadAccount(prefs) + val account = AccountUtils.getAccount(applicationContext) val startAllowed = checkStartConditions(account, prefs, requestCollector) if (!startAllowed) { @@ -65,12 +64,12 @@ class FullScanWorker(private val context: Context, private val workerParams: Wor return Result.success() } - val remoteSyncRequests = scanRemoteFiles(account, syncFolders) + val remoteSyncRequests = scanRemoteFiles(account, syncFolders.toMutableList()) syncRequests.putAll(remoteSyncRequests) Timber.d("${remoteSyncRequests.size} request collected from cloud") - val localSyncRequests = scanLocalFiles(syncFolders) + val localSyncRequests = scanLocalFiles(syncFolders.toMutableList()) syncRequests.putAll(localSyncRequests) Timber.d("${localSyncRequests.size} request collected from device") @@ -115,13 +114,6 @@ class FullScanWorker(private val context: Context, private val workerParams: Wor return true } - private fun loadAccount(prefs: SharedPreferences): Account? { - val accountName = prefs.getString(AccountManager.KEY_ACCOUNT_NAME, "") ?: return null - val accountType = context.getString(R.string.eelo_account_type) - - return CommonUtils.getAccount(accountName, accountType, AccountManager.get(context)) - } - /** * indicate if minimum delay between two periodic scan is respected */ diff --git a/app/src/main/java/foundation/e/drive/periodicScan/ListAppsWorker.java b/app/src/main/java/foundation/e/drive/periodicScan/ListAppsWorker.java index 6c6b9062f1efa9be72c633f7b855443c898bcfef..ee492393ecb892a49d90d757469bec42455cb6af 100644 --- a/app/src/main/java/foundation/e/drive/periodicScan/ListAppsWorker.java +++ b/app/src/main/java/foundation/e/drive/periodicScan/ListAppsWorker.java @@ -76,7 +76,10 @@ public class ListAppsWorker extends Worker { final Cursor cursor = context.getContentResolver().query( Uri.parse(PWA_PLAYER), null, null, null, null); - if (cursor.getCount() <= 0) return; + if (cursor.getCount() <= 0) { + cursor.close(); + return; + } stringBuilder.append(PWA_SECTION_SEPARATOR); cursor.moveToFirst(); diff --git a/app/src/main/java/foundation/e/drive/periodicScan/PeriodicScanWorker.java b/app/src/main/java/foundation/e/drive/periodicScan/PeriodicScanWorker.java index ffaf89c664679dfe4d7dfe296b43381fbba9a216..bcfcfbe9fcc37e808cfe15b0e430570027a884f1 100644 --- a/app/src/main/java/foundation/e/drive/periodicScan/PeriodicScanWorker.java +++ b/app/src/main/java/foundation/e/drive/periodicScan/PeriodicScanWorker.java @@ -8,9 +8,6 @@ package foundation.e.drive.periodicScan; -import static foundation.e.drive.work.WorkRequestFactory.WorkType.ONE_TIME_APP_LIST; -import static foundation.e.drive.work.WorkRequestFactory.WorkType.ONE_TIME_FULL_SCAN; - import android.content.Context; import androidx.annotation.NonNull; @@ -23,6 +20,7 @@ import androidx.work.WorkerParameters; import java.util.ArrayList; import java.util.List; +import foundation.e.drive.work.OneTimeWorkType; import foundation.e.drive.work.WorkRequestFactory; import timber.log.Timber; @@ -43,10 +41,11 @@ public class PeriodicScanWorker extends Worker { public Result doWork() { try { final WorkManager workManager = WorkManager.getInstance(getApplicationContext()); + final WorkRequestFactory workRequestFactory = WorkRequestFactory.INSTANCE; final List workRequestsLists = new ArrayList<>(); - workRequestsLists.add(WorkRequestFactory.getOneTimeWorkRequest(ONE_TIME_APP_LIST, null)); - workRequestsLists.add(WorkRequestFactory.getOneTimeWorkRequest(ONE_TIME_FULL_SCAN, null)); + workRequestsLists.add(workRequestFactory.createOneTimeWorkRequest(OneTimeWorkType.LIST_APPS, null)); + workRequestsLists.add(workRequestFactory.createOneTimeWorkRequest(OneTimeWorkType.FULL_SCAN, null)); workManager.beginUniqueWork(FullScanWorker.UNIQUE_WORK_NAME, ExistingWorkPolicy.KEEP, workRequestsLists) .enqueue(); diff --git a/app/src/main/java/foundation/e/drive/periodicScan/contentScanner/FileDiffUtils.kt b/app/src/main/java/foundation/e/drive/periodicScan/contentScanner/FileDiffUtils.kt index 41e34693a48ec0db9eaecad4105ac9f66bdb97b6..5019cfbf43c939401e53c181b3ce85b63eb57166 100644 --- a/app/src/main/java/foundation/e/drive/periodicScan/contentScanner/FileDiffUtils.kt +++ b/app/src/main/java/foundation/e/drive/periodicScan/contentScanner/FileDiffUtils.kt @@ -24,7 +24,7 @@ object FileDiffUtils { @JvmStatic fun getActionForFileDiff(remoteFile: RemoteFile, fileState: SyncedFileState): Action { if (!hasEtagChanged(remoteFile, fileState)) { - if (isCorruptedTimestamp(remoteFile.modifiedTimestamp / 1000)) return Action.Upload + if (isCorruptedTimestamp(remoteFile.modifiedTimestamp)) return Action.Upload if (hasAlreadyBeenDownloaded(fileState)) return Action.Skip } @@ -93,13 +93,13 @@ object FileDiffUtils { * * For yet unknown reason, some remote files have this value on cloud (DB & file system) * the only way to fix them is to force re upload of the file with correct value - * @param timestampInSecond remote file timestamp (Long) + * @param timestampInMillisecond remote file timestamp (Long) * @return true if the timestamp is equal to max of unsigned int 32 */ @VisibleForTesting @JvmStatic - fun isCorruptedTimestamp(timestampInSecond: Long): Boolean { - return timestampInSecond >= AppConstants.CORRUPTED_TIMESTAMP_IN_SECOND + fun isCorruptedTimestamp(timestampInMillisecond: Long): Boolean { + return timestampInMillisecond >= AppConstants.CORRUPTED_TIMESTAMP_IN_MILLISECOND } /** diff --git a/app/src/main/java/foundation/e/drive/periodicScan/contentScanner/LocalFileLister.java b/app/src/main/java/foundation/e/drive/periodicScan/contentScanner/LocalFileLister.java index 11d2c58d891490445fbf7b8caf58c80caad28899..43bc066206043522bb0ed9bd862b3c06d169772e 100644 --- a/app/src/main/java/foundation/e/drive/periodicScan/contentScanner/LocalFileLister.java +++ b/app/src/main/java/foundation/e/drive/periodicScan/contentScanner/LocalFileLister.java @@ -105,7 +105,7 @@ public class LocalFileLister extends AbstractFileLister { folder = new FolderWrapper(dir); final String category = syncedFolder.isMediaType() ? "media" : syncedFolder.getLibelle(); - final FileFilter filter = FileFilterFactory.getFileFilter(category); + final FileFilter filter = FileFilterFactory.INSTANCE.buildFileFilter(category); final File[] files = dir.listFiles(filter); if (files != null) { folder.addContent(Arrays.asList(files)); diff --git a/app/src/main/java/foundation/e/drive/receivers/BootCompletedReceiver.java b/app/src/main/java/foundation/e/drive/receivers/BootCompletedReceiver.java index 31e339f57c5b9b03bfadab5f9e7ff666825bbbb3..5001e7f5bf275ad3a3dd3735173abcc37a3b5b00 100644 --- a/app/src/main/java/foundation/e/drive/receivers/BootCompletedReceiver.java +++ b/app/src/main/java/foundation/e/drive/receivers/BootCompletedReceiver.java @@ -1,5 +1,5 @@ /* - * Copyright © MURENA SAS 2022-2023. + * Copyright © MURENA SAS 2022-2024. * 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 @@ -16,14 +16,15 @@ import android.content.SharedPreferences; import android.database.sqlite.SQLiteException; import androidx.annotation.NonNull; +import androidx.work.WorkManager; import foundation.e.drive.BuildConfig; import foundation.e.drive.database.DbHelper; import foundation.e.drive.synchronization.SyncProxy; -import foundation.e.drive.utils.AccountUtils; import foundation.e.drive.utils.AppConstants; import foundation.e.drive.utils.CommonUtils; -import foundation.e.drive.utils.WorkerUtils; +import foundation.e.drive.work.WorkLauncher; +import foundation.e.drive.account.AccountUtils; import timber.log.Timber; /** @@ -33,57 +34,106 @@ import timber.log.Timber; public class BootCompletedReceiver extends BroadcastReceiver { private static final String DATE_SYSTEM_PROPERTY = "ro.build.date"; private static final String PREF_VERSION_CODE = "VERSION_CODE"; + private static final String OLD_SETUP_COMPLETED_PREF_KEY ="initService_has_run"; + + private static final int VERSION_CODE_FOR_UPDATE_1 = 1002000; + private static final int VERSION_CODE_FOR_UPDATE_2 = 1003017; + + @Override public void onReceive(@NonNull Context context, @NonNull Intent intent) { final String action = intent.getAction(); Timber.v("onReceive(...)"); - final SharedPreferences pref = context.getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE); - if (Intent.ACTION_BOOT_COMPLETED.equals(action)) { - final String currentDateProp = CommonUtils.getProp(DATE_SYSTEM_PROPERTY); + if (!Intent.ACTION_BOOT_COMPLETED.equals(action) + || !AccountUtils.isAccountAvailable(context)) { + return; + } - if (isOsUpdated(pref, currentDateProp)) { // App is persistent so can only be updated (by replacement) on OS update - handleOsUpdate(context); - } + final SharedPreferences prefs = context.getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE); + final WorkLauncher workLauncher = WorkLauncher.getInstance(context); - final boolean isSetupDone = AccountUtils.isSetupAlreadyDone(pref); - final boolean isAccountPresent = AccountUtils.isAccountPresent(context); + migrateSetupCompletedPrefsKey(prefs); + if (!isSetupCompleted(prefs)){ + workLauncher.enqueueSetupWorkers(context); + return; + } - if (isSetupDone && BuildConfig.VERSION_CODE > pref.getInt(PREF_VERSION_CODE, 1002000)) { - pref.edit().putInt(PREF_VERSION_CODE, BuildConfig.VERSION_CODE).apply(); - try { - DbHelper.cleanSyncedFileStateTableAfterUpdate(context); - } catch (SQLiteException exception) { - Timber.e(exception); - } - } + /* + * App was persistent so can only be updated (by replacement) on OS update + * note: still needs the below check and call when migrating for persistent eDrive to not persistent eDrive. + * But on "not persistent eDrive" update "to not persistent eDrive" update+1, this won't be required again + */ + final String currentDateProp = CommonUtils.getProp(DATE_SYSTEM_PROPERTY); + if (isOsUpdated(prefs, currentDateProp)) { + handleOsUpdate(context); + } + + final int lastSavedVersionCode = prefs.getInt(PREF_VERSION_CODE, VERSION_CODE_FOR_UPDATE_1); + if (isEdriveUpdated(lastSavedVersionCode)) { + onEdriveUpdate(lastSavedVersionCode, context); + prefs.edit().putInt(PREF_VERSION_CODE, BuildConfig.VERSION_CODE).apply(); + } + + SyncProxy.INSTANCE.startListeningFiles((Application) context.getApplicationContext()); + + workLauncher.enqueuePeriodicFullScan(); + workLauncher.enqueuePeriodicUserInfoFetching(); + } + + private void migrateSetupCompletedPrefsKey(@NonNull SharedPreferences prefs) { + if (prefs.getBoolean(OLD_SETUP_COMPLETED_PREF_KEY, false)) { + Timber.i("Update setup complete preferences"); + prefs + .edit() + .remove(OLD_SETUP_COMPLETED_PREF_KEY) + .putBoolean(AppConstants.SETUP_COMPLETED, true) + .apply(); + } + } + + /** @noinspection SpellCheckingInspection*/ + private boolean isEdriveUpdated(int oldVersionCode) { + Timber.d("isEdriveUpdated (%s > %s) ?", BuildConfig.VERSION_CODE, oldVersionCode); + return BuildConfig.VERSION_CODE > oldVersionCode; + } - if (isAccountPresent) { - WorkerUtils.INSTANCE.registerSetupWorkers(context.getApplicationContext()); + private void onEdriveUpdate(int oldVersionCode, @NonNull Context context) { + if (oldVersionCode <= VERSION_CODE_FOR_UPDATE_1) { + try { + DbHelper.cleanSyncedFileStateTableAfterUpdate(context); + } catch (SQLiteException exception) { + Timber.e(exception); } + } + + if (oldVersionCode <= VERSION_CODE_FOR_UPDATE_2) { + Timber.d("Triggered the update 2 from: %s", oldVersionCode); + final WorkManager workManager= WorkManager.getInstance(context); + workManager.cancelAllWork(); - SyncProxy.INSTANCE.startListeningFiles((Application) context.getApplicationContext()); + final WorkLauncher workLauncher = WorkLauncher.getInstance(context); + workLauncher.enqueuePeriodicFullScan(); + workLauncher.enqueuePeriodicUserInfoFetching(); } } - private void forceDBUpdate(@NonNull Context context) { - final DbHelper dbHelper = new DbHelper(context); - dbHelper.getWritableDatabase().close(); //Force upgrade of db. + private boolean isOsUpdated(@NonNull SharedPreferences prefs, @NonNull String currentDateProp) { + final String lastKnownDateProp = prefs.getString(DATE_SYSTEM_PROPERTY, ""); + return !currentDateProp.equals(lastKnownDateProp); } /** * Force reinitialization, upgrade of DB in case of OS update - * todo remove when setPersistentFlag=true will be removed - * * @param context Context used to start InitializationService */ private void handleOsUpdate(@NonNull Context context) { - forceDBUpdate(context); + final DbHelper dbHelper = new DbHelper(context); + dbHelper.getWritableDatabase().close(); //force DB update } - private boolean isOsUpdated(@NonNull SharedPreferences prefs, @NonNull String currentDateProp) { - final String lastKnownDateProp = prefs.getString(DATE_SYSTEM_PROPERTY, ""); - return !currentDateProp.equals(lastKnownDateProp); + private boolean isSetupCompleted(@NonNull SharedPreferences prefs) { + return prefs.getBoolean(AppConstants.SETUP_COMPLETED, false); } } diff --git a/app/src/main/java/foundation/e/drive/receivers/DebugCmdReceiver.java b/app/src/main/java/foundation/e/drive/receivers/DebugCmdReceiver.java index b4b6cf2956e2b002cc6f0c56f96b7809ab332c88..412f0339e57e0481dbddd2fbcae06301f15aa137 100644 --- a/app/src/main/java/foundation/e/drive/receivers/DebugCmdReceiver.java +++ b/app/src/main/java/foundation/e/drive/receivers/DebugCmdReceiver.java @@ -7,21 +7,30 @@ */ package foundation.e.drive.receivers; -import static foundation.e.drive.work.WorkRequestFactory.WorkType.ONE_TIME_FORCED_FULL_SCAN; - import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.os.Environment; import androidx.annotation.NonNull; -import androidx.work.ExistingWorkPolicy; -import androidx.work.OneTimeWorkRequest; import androidx.work.WorkManager; +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import foundation.e.drive.EdriveApplication; +import foundation.e.drive.account.AccountUtils; import foundation.e.drive.database.DbHelper; -import foundation.e.drive.periodicScan.FullScanWorker; +import foundation.e.drive.fileObservers.FileObserverManager; +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.synchronization.SyncRequestCollector; import foundation.e.drive.utils.ReleaseTree; -import foundation.e.drive.work.WorkRequestFactory; +import foundation.e.drive.work.WorkLauncher; import timber.log.Timber; /** @@ -30,21 +39,24 @@ import timber.log.Timber; */ public class DebugCmdReceiver extends BroadcastReceiver { - public static final String ACTION_FORCE_SYNC = "foundation.e.drive.action.FORCE_SYNC"; + public static final String ACTION_TEST_SYNC ="foundation.e.drive.action.TEST_SYNC"; + public static final String ACTION_FORCE_SCAN = "foundation.e.drive.action.FORCE_SCAN"; public static final String ACTION_DUMP_DATABASE = "foundation.e.drive.action.DUMP_DATABASE"; public static final String ACTION_FULL_LOG_ON_PROD = "foundation.e.drive.action.FULL_LOG_ON_PROD"; + public static final String ACTION_ENABLE_FILE_OBSERVER = "foundation.e.drive.action.ENABLE_FILE_OBSERVER"; + private static final String FULL_LOG_ENABLE_KEY = "full_log_enable"; + private static final String FILE_OBSERVER_ENABLE_KEY = "file_observer_enable"; + @Override public void onReceive(@NonNull Context context, @NonNull Intent intent) { Timber.tag(DebugCmdReceiver.class.getSimpleName()).v("onReceive"); + final WorkManager workManager = WorkManager.getInstance(context); + switch (intent.getAction()) { - case ACTION_FORCE_SYNC: + case ACTION_FORCE_SCAN: Timber.d("Force Sync intent received"); - final WorkManager workManager = WorkManager.getInstance(context); - final OneTimeWorkRequest fullScanWorkRequest = WorkRequestFactory.getOneTimeWorkRequest(ONE_TIME_FORCED_FULL_SCAN, null); - workManager.enqueueUniqueWork(FullScanWorker.UNIQUE_WORK_NAME, - ExistingWorkPolicy.KEEP, - fullScanWorkRequest); + WorkLauncher.getInstance(context).enqueueOneTimeFullScan(true); break; case ACTION_DUMP_DATABASE: Timber.d("Dump database intent received"); @@ -55,8 +67,65 @@ public class DebugCmdReceiver extends BroadcastReceiver { ReleaseTree.allowDebugLogOnProd(allow_full_log); Timber.d("Allow full log on prod: %s", allow_full_log); break; + case ACTION_ENABLE_FILE_OBSERVER: + if (AccountUtils.getAccount(context) == null) { + Timber.d("Ignore intent: no account"); + } + + final EdriveApplication application = (EdriveApplication) context.getApplicationContext(); + final boolean enabled = intent.getBooleanExtra(FILE_OBSERVER_ENABLE_KEY, true); + Timber.i("Intent received: enable FileObserver: %s", enabled); + if (enabled) { + application.startRecursiveFileObserver(); + } else { + application.stopRecursiveFileObserver(); + } + break; + case ACTION_TEST_SYNC: + Timber.d("Test SyncWorker.kt started"); + final SyncRequestCollector collector = (SyncRequestCollector) SyncProxy.INSTANCE; + collector.onPeriodicScanStart((EdriveApplication) context.getApplicationContext()); + + final File dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS); + final List requests = generateDummyFileToSync(dir, context); + + if (requests.isEmpty()) return; + + collector.queueSyncRequests(requests, context.getApplicationContext()); + + collector.startSynchronization(context.getApplicationContext()); + break; default: break; } } + + private List generateDummyFileToSync(File dir, Context context) { + final List result = new ArrayList<>(); + final int fileAmount = 10; + final SyncedFolder parentFolder = DbHelper.getSyncedFolderByLocalPath(dir.getAbsolutePath(), context); + + if (parentFolder == null || !parentFolder.isEnabled()) { + Timber.d("Won't send sync request: no parent are known for n %s", dir.getAbsolutePath()); + return result; + } + + for (int index = 0; index < fileAmount; index++) { + final String fileName = "a" + System.currentTimeMillis()/1200 + ".txt"; + File file = new File(dir.getAbsolutePath(), fileName); + try { + file.createNewFile(); + } catch (IOException exception) { + Timber.d("can't create file"); + continue; + } + + final String remotePath = parentFolder.getRemoteFolder() + fileName; + final SyncedFileState fileState = new SyncedFileState(-1, fileName, file.getAbsolutePath(), remotePath, "", 0L, parentFolder.getId(), true, 3); + + final SyncRequest request = new SyncRequest(fileState, SyncRequest.Type.UPLOAD); + result.add(request); + } + return result; + } } \ No newline at end of file diff --git a/app/src/main/java/foundation/e/drive/synchronization/SyncProgressNotifier.kt b/app/src/main/java/foundation/e/drive/synchronization/SyncProgressNotifier.kt new file mode 100644 index 0000000000000000000000000000000000000000..79772ea3784ff5b70a715e79d1899aecfc488cb1 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/synchronization/SyncProgressNotifier.kt @@ -0,0 +1,73 @@ +/* + * 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.synchronization + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.content.pm.ServiceInfo +import android.os.Build +import androidx.core.app.NotificationCompat +import androidx.work.ForegroundInfo +import foundation.e.drive.R + +class SyncProgressNotifier(private val context: Context) { + companion object { + const val NOTIFICATION_ID = 2003004 + const val NOTIF_CHANNEL_ID = "syncChannelId" + } + + private val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + private fun createNotification(requestCounter: Int): Notification { + val text = getNotificationText(requestCounter) + val title = context.getString(R.string.notif_sync_is_running_title) + + return NotificationCompat.Builder(context, NOTIF_CHANNEL_ID) + .setOngoing(true) + .setContentTitle(title) + .setContentText(text) + .setSmallIcon(R.drawable.ic_synchronization) + .build() + } + + fun notifyTaskFinished(requestCounter: Int) { + notificationManager.notify(NOTIFICATION_ID, createNotification(requestCounter)) + } + + fun cancelAllSyncNotifications() { + notificationManager.cancel(NOTIFICATION_ID) + } + + private fun getNotificationText(requestCount: Int): String { + return context.resources + .getQuantityString(R.plurals.notif_sync_is_running_txt, requestCount, requestCount) + } + + fun createNotificationChannel() { + val channelName = context.getString(R.string.notif_sync_channel_name) + val importance = NotificationManager.IMPORTANCE_MIN + val channel = NotificationChannel(NOTIF_CHANNEL_ID, channelName, importance) + val channelDescription = context.getString(R.string.notif_sync_channel_description) + channel.description = channelDescription + + notificationManager.createNotificationChannel(channel) + } + + internal fun createForegroundInfo(requestCount: Int): ForegroundInfo { + val notification = createNotification(requestCount) + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ForegroundInfo(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) + } else { + ForegroundInfo(NOTIFICATION_ID, notification) + } + } +} 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 568e37f65ca499fa6e422a8dda72eee127f7850b..903d5223e4f23fb4709c36a85fde8c1693625ba2 100644 --- a/app/src/main/java/foundation/e/drive/synchronization/SyncProxy.kt +++ b/app/src/main/java/foundation/e/drive/synchronization/SyncProxy.kt @@ -9,14 +9,11 @@ package foundation.e.drive.synchronization import android.app.Application import android.content.Context -import androidx.work.ExistingWorkPolicy -import androidx.work.WorkManager import foundation.e.drive.EdriveApplication import foundation.e.drive.database.FailedSyncPrefsManager import foundation.e.drive.models.SyncRequest import foundation.e.drive.models.SyncWrapper -import foundation.e.drive.work.WorkRequestFactory -import foundation.e.drive.work.WorkRequestFactory.WorkType.ONE_TIME_SYNC +import foundation.e.drive.work.WorkLauncher import timber.log.Timber import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentLinkedQueue @@ -116,11 +113,13 @@ object SyncProxy: SyncRequestCollector, SyncManager { override fun startSynchronization(context: Context) { if (context !is EdriveApplication) { Timber.d("Invalid parameter: startSynchronization(context)") + StateMachine.changeState(SyncState.LISTENING_FILES) return } if (syncRequestQueue.isEmpty()) { Timber.d("Request queue is empty") + startListeningFiles(context) return } @@ -129,15 +128,10 @@ object SyncProxy: SyncRequestCollector, SyncManager { if (!isStateChanged) return - if (previousSyncState == SyncState.PERIODIC_SCAN) { - context.startRecursiveFileObserver() - } + context.startRecursiveFileObserver() if (previousSyncState != SyncState.SYNCHRONIZING) { - val workManager = WorkManager.getInstance(context) - val workRequest = WorkRequestFactory.getOneTimeWorkRequest(ONE_TIME_SYNC, null) - - workManager.enqueueUniqueWork(SyncWorker.UNIQUE_WORK_NAME, ExistingWorkPolicy.KEEP, workRequest) + WorkLauncher.getInstance(context).enqueueOneTimeSync(); } } @@ -155,8 +149,6 @@ object SyncProxy: SyncRequestCollector, SyncManager { val isStateChanged = StateMachine.changeState(SyncState.PERIODIC_SCAN) if (!isStateChanged) return false - application.stopRecursiveFileObserver() - return true } @@ -171,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() } /* diff --git a/app/src/main/java/foundation/e/drive/synchronization/SyncTask.kt b/app/src/main/java/foundation/e/drive/synchronization/SyncTask.kt index d37317e846da5cc1d6433ff03a50f7a35a58ec43..7a43489f3fd09f217f6d547edb77b47a71cc4a2f 100644 --- a/app/src/main/java/foundation/e/drive/synchronization/SyncTask.kt +++ b/app/src/main/java/foundation/e/drive/synchronization/SyncTask.kt @@ -17,7 +17,9 @@ import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCo import foundation.e.drive.database.DbHelper import foundation.e.drive.database.FailedSyncPrefsManager import foundation.e.drive.models.SyncRequest -import foundation.e.drive.models.SyncRequest.Type.* +import foundation.e.drive.models.SyncRequest.Type.UPLOAD +import foundation.e.drive.models.SyncRequest.Type.DISABLE_SYNCING +import foundation.e.drive.models.SyncRequest.Type.DOWNLOAD import foundation.e.drive.models.SyncWrapper import foundation.e.drive.synchronization.tasks.UploadFileOperation import foundation.e.drive.utils.CommonUtils @@ -36,6 +38,8 @@ class SyncTask( private val fileName = request.syncedFileState.name private val fileLocalPath = request.syncedFileState.localPath + private val syncNotifier = SyncProgressNotifier(context) + override fun run() { if (!canStart()) { @@ -53,6 +57,7 @@ class SyncTask( DISABLE_SYNCING -> runSyncDisabling() } + syncNotifier.notifyTaskFinished(SyncWorker.pendingTaskCounter.decrementAndGet()) updateFailureCounter(request, succeed) syncManager.removeStartedRequest(fileLocalPath) Timber.d("${request.operationType.name} finished for $fileLocalPath") @@ -143,7 +148,7 @@ class SyncTask( if (!success) { if (request.operationType == SyncRequest.Type.UPLOAD) { val filePath = fileState.localPath - if (filePath.isEmpty()) return; + if (filePath.isEmpty()) return val file = File(filePath) if (file.length() >= UploadFileOperation.FILE_SIZE_FLOOR_FOR_CHUNKED) return } diff --git a/app/src/main/java/foundation/e/drive/synchronization/SyncWorker.kt b/app/src/main/java/foundation/e/drive/synchronization/SyncWorker.kt index 2eaa32fcdaeef09f4bbc5bf9e6d5b6555c330eb8..352a2774dc82f9ace7c017ee1c53629ff10b022d 100644 --- a/app/src/main/java/foundation/e/drive/synchronization/SyncWorker.kt +++ b/app/src/main/java/foundation/e/drive/synchronization/SyncWorker.kt @@ -8,26 +8,19 @@ package foundation.e.drive.synchronization import android.accounts.Account -import android.accounts.AccountManager -import android.app.NotificationChannel -import android.app.NotificationManager import android.content.Context -import android.content.Context.NOTIFICATION_SERVICE -import androidx.core.app.NotificationCompat -import androidx.work.ForegroundInfo import androidx.work.Worker import androidx.work.WorkerParameters import com.owncloud.android.lib.common.OwnCloudClient import foundation.e.drive.EdriveApplication -import foundation.e.drive.R -import foundation.e.drive.utils.AppConstants -import foundation.e.drive.utils.CommonUtils +import foundation.e.drive.account.AccountUtils import foundation.e.drive.utils.DavClientProvider import timber.log.Timber import java.util.concurrent.Executors import java.util.concurrent.Future import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicInteger class SyncWorker( context: Context, @@ -36,20 +29,19 @@ class SyncWorker( companion object { const val UNIQUE_WORK_NAME = "syncWorker" - const val NOTIF_CHANNEL_ID = "syncChannelId" - const val NOTIFICATION_ID = 2003004 + internal lateinit var pendingTaskCounter: AtomicInteger private const val threadAmount = 2 private val syncManager = SyncProxy as SyncManager } private var account: Account? = null private var ocClient: OwnCloudClient? = null - private val notificationManager = applicationContext.getSystemService(NOTIFICATION_SERVICE) as NotificationManager private var executor = Executors.newFixedThreadPool(threadAmount) + private val syncNotifier = SyncProgressNotifier(applicationContext) override fun onStopped() { Timber.d("SyncWorker has been stopped") - notificationManager.cancel(NOTIFICATION_ID) + syncNotifier.cancelAllSyncNotifications() executor.shutdownNow() super.onStopped() @@ -57,7 +49,7 @@ class SyncWorker( override fun doWork(): Result { try { - account = loadAccount() + account = AccountUtils.getAccount(applicationContext) if (account == null) { Timber.d("Warning : account is null") syncManager.startListeningFiles(applicationContext as EdriveApplication) @@ -70,15 +62,16 @@ class SyncWorker( syncManager.startListeningFiles(applicationContext as EdriveApplication) return Result.failure() } - - createNotificationChannel() + syncNotifier.createNotificationChannel() while (!syncManager.isQueueEmpty()) { - setForegroundAsync(createForegroundInfo()) + val requestCount = syncManager.getQueueSize() + pendingTaskCounter = AtomicInteger(requestCount) + setForegroundAsync(syncNotifier.createForegroundInfo(requestCount)) executeRequests() } - notificationManager.cancel(NOTIFICATION_ID) + syncNotifier.cancelAllSyncNotifications() syncManager.startListeningFiles(applicationContext as EdriveApplication) DavClientProvider.getInstance().saveAccounts(applicationContext) @@ -89,15 +82,6 @@ class SyncWorker( return Result.success() } - private fun loadAccount(): Account? { - val context = applicationContext - val prefs = context.getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE) - val accountName = prefs.getString(AccountManager.KEY_ACCOUNT_NAME, "") - val accountType = context.getString(R.string.eelo_account_type) - //for above: AS complaint about accountName as String? while it can't be... - return CommonUtils.getAccount(accountName!!, accountType, AccountManager.get(context)) - } - private fun getOcClient(account: Account): OwnCloudClient? { return DavClientProvider.getInstance().getClientInstance( account, @@ -131,31 +115,4 @@ class SyncWorker( executor.awaitTermination(30, TimeUnit.SECONDS) } } - - private fun createForegroundInfo(): ForegroundInfo { - val title = applicationContext.getString(R.string.notif_sync_is_running_title) - val requestCount = syncManager.getQueueSize() - val text = applicationContext.resources - .getQuantityString(R.plurals.notif_sync_is_running_txt, requestCount, requestCount) - - val notification = NotificationCompat.Builder(applicationContext, NOTIF_CHANNEL_ID) - .setOngoing(true) - .setContentTitle(title) - .setContentText(text) - .setSmallIcon(R.drawable.ic_synchronization) - - .build() - - return ForegroundInfo(NOTIFICATION_ID, notification) - } - - private fun createNotificationChannel() { - val channelName = applicationContext.getString(R.string.notif_sync_channel_name) - val importance = NotificationManager.IMPORTANCE_MIN - val channel = NotificationChannel(NOTIF_CHANNEL_ID, channelName, importance) - val channelDescription = applicationContext.getString(R.string.notif_sync_channel_description) - channel.description = channelDescription - - notificationManager.createNotificationChannel(channel) - } } \ No newline at end of file diff --git a/app/src/main/java/foundation/e/drive/synchronization/tasks/DownloadFileOperation.java b/app/src/main/java/foundation/e/drive/synchronization/tasks/DownloadFileOperation.java index 4874e4609c6c33b24d6f9f76e6a4cc0d14ac8bbd..7399d54d283499dcc246d3247770a3abb28a86b4 100644 --- a/app/src/main/java/foundation/e/drive/synchronization/tasks/DownloadFileOperation.java +++ b/app/src/main/java/foundation/e/drive/synchronization/tasks/DownloadFileOperation.java @@ -19,7 +19,7 @@ import static com.owncloud.android.lib.common.operations.RemoteOperationResult.R import static java.nio.file.StandardCopyOption.REPLACE_EXISTING; -import static foundation.e.drive.utils.AppConstants.CORRUPTED_TIMESTAMP_IN_SECOND; +import static foundation.e.drive.utils.AppConstants.CORRUPTED_TIMESTAMP_IN_MILLISECOND; import android.content.Context; import android.media.MediaScannerConnection; @@ -159,7 +159,7 @@ public class DownloadFileOperation extends RemoteOperation { return FORBIDDEN; } - if (remoteFile.getModifiedTimestamp() < CORRUPTED_TIMESTAMP_IN_SECOND) { + if (remoteFile.getModifiedTimestamp() < CORRUPTED_TIMESTAMP_IN_MILLISECOND) { targetFile.setLastModified(remoteFile.getModifiedTimestamp()); } syncedFileState.setLastModified(targetFile.lastModified()); @@ -185,10 +185,14 @@ public class DownloadFileOperation extends RemoteOperation { final Path tmpPath = tmpFile.toPath(); final Path copyResult = Files.copy(tmpPath, targetPath, REPLACE_EXISTING); - if (copyResult.toFile().length() == tmpFile.length()) { + final File copyResultFile = copyResult.toFile(); + + if (copyResultFile.length() == tmpFile.length()) { tmpFile.delete(); return true; } + + copyResultFile.delete(); } catch (NoSuchFileException exception) { Timber.w(exception); } catch (IOException | SecurityException | NullPointerException exception) { diff --git a/app/src/main/java/foundation/e/drive/synchronization/tasks/UploadFileOperation.java b/app/src/main/java/foundation/e/drive/synchronization/tasks/UploadFileOperation.java index 5f5ec8593eea6734103223db30f0f626079b3fb4..9993d2ad756d2c9bc49df158dab4568e8dc60b95 100644 --- a/app/src/main/java/foundation/e/drive/synchronization/tasks/UploadFileOperation.java +++ b/app/src/main/java/foundation/e/drive/synchronization/tasks/UploadFileOperation.java @@ -99,13 +99,13 @@ public class UploadFileOperation extends RemoteOperation { final ResultCode resultCode; if (uploadResult.isSuccess()) { - updateSyncedFileState(uploadResult, file.lastModified(), client); + final String latestEtag = getLatestEtag(uploadResult, client); + updateSyncedFileState(latestEtag, file.lastModified()); resultCode = uploadResult.getCode(); } else { resultCode = onUploadFailure(uploadResult.getCode(), file.getName()); } - DbHelper.manageSyncedFileStateDB(syncedState, "UPDATE", context); return new RemoteOperationResult(resultCode); } @@ -182,30 +182,49 @@ public class UploadFileOperation extends RemoteOperation { /** * Update syncedFileState (etag & last modified) in case of successful upload - * @param uploadResult The Upload's result instance + * @param latestEtag new eTag of the remoteFile * @param fileLastModified value of local file's last modified - * @param client The client used to check etag if missing */ - private void updateSyncedFileState(final RemoteOperationResult uploadResult, final long fileLastModified, final OwnCloudClient client) { - //The below if statement should only be called for chunked upload. But + private void updateSyncedFileState(@Nullable final String latestEtag, final long fileLastModified) { + if (latestEtag != null) { + syncedState.setLastEtag(latestEtag); + } + + syncedState.setLastModified(fileLastModified); + DbHelper.manageSyncedFileStateDB(syncedState, "UPDATE", context); + } + + private @Nullable String getLatestEtag(@NonNull final RemoteOperationResult uploadResult, @NonNull final OwnCloudClient client) { + + if (uploadResult.getResultData() != null) { + return uploadResult.getResultData(); + } + + //The below code should only be called for chunked upload. But //for some unknown reason, the simple file upload doesn't give the eTag in the result // so, I moved the code here as a security - if (uploadResult.getResultData() == null) { - final RemoteOperationResult result = readRemoteFile(syncedState.getRemotePath(), client); - final ArrayList resultData = result.getData(); - if (result.isSuccess() && resultData != null && !resultData.isEmpty()) { - final String latestETag = ((RemoteFile) resultData.get(0)).getEtag(); - uploadResult.setResultData(latestETag); - } + try { + return readLatestEtagFromCloud(client); + } catch (ClassCastException exception) { + Timber.w("Impossible to read eTag from cloud: %s", exception.getMessage()); + return null; } - final String etag = uploadResult.getResultData(); + } + + private @Nullable String readLatestEtagFromCloud(@NonNull final OwnCloudClient client) throws ClassCastException{ + final RemoteOperationResult result = readRemoteFile(syncedState.getRemotePath(), client); + if (!result.isSuccess()) return null; - if (etag != null) { - syncedState.setLastEtag(etag); + final ArrayList resultData = result.getData(); + + if (resultData == null || resultData.isEmpty()) { + return null; } - syncedState.setLastModified(fileLastModified); + + return ((RemoteFile) resultData.get(0)).getEtag(); } + /** * Perform an operation to check available space on server before to upload * @param client OwnCloudClient @@ -266,7 +285,7 @@ public class UploadFileOperation extends RemoteOperation { @VisibleForTesting() @NonNull public RemoteOperationResult uploadChunkedFile(@NonNull final File file, @NonNull final OwnCloudClient client) { - final long timeStamp = file.lastModified() / 1000; + final long timeStamp = formatTimestampToMatchCloud(file.lastModified()); final String mimeType = getMimeType(file); final ChunkedFileUploadRemoteOperation uploadOperation = new ChunkedFileUploadRemoteOperation(syncedState.getLocalPath(), syncedState.getRemotePath(), @@ -323,7 +342,7 @@ public class UploadFileOperation extends RemoteOperation { @VisibleForTesting() @NonNull public RemoteOperationResult uploadFile(@NonNull final File file, @NonNull final OwnCloudClient client, boolean checkEtag) { - final long timeStamp = file.lastModified() / 1000; + final long timeStamp = formatTimestampToMatchCloud(file.lastModified()); final String eTag = checkEtag ? syncedState.getLastEtag() : null; final UploadFileRemoteOperation uploadOperation = new UploadFileRemoteOperation(syncedState.getLocalPath(), @@ -361,7 +380,7 @@ public class UploadFileOperation extends RemoteOperation { * @return String timestamp in second */ @VisibleForTesting() - public @NonNull String formatTimestampToMatchCloud(long timestamp) { - return String.valueOf(timestamp/1000); + public long formatTimestampToMatchCloud(long timestamp) { + return timestamp / 1000; } } \ No newline at end of file diff --git a/app/src/main/java/foundation/e/drive/utils/AccountUtils.java b/app/src/main/java/foundation/e/drive/utils/AccountUtils.java deleted file mode 100644 index 740b027592f49e3c185a2ef95689a5a50081b353..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/drive/utils/AccountUtils.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright MURENA SAS 2022 - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package foundation.e.drive.utils; - -import static foundation.e.drive.utils.AppConstants.ACCOUNT_DATA_GROUPS; - -import android.accounts.Account; -import android.accounts.AccountManager; -import android.content.Context; -import android.content.SharedPreferences; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.util.Arrays; -import java.util.Optional; - -import foundation.e.drive.R; - -public class AccountUtils { - - - @NonNull - public static Optional getPremiumGroup(@NonNull AccountManager accountManager, @Nullable Account account) { - if (account == null) { - return Optional.empty(); - } - - final String groupData = accountManager.getUserData(account, ACCOUNT_DATA_GROUPS); - - if (groupData == null || groupData.isEmpty()) { - return Optional.empty(); - } - - final String[] groups = groupData.split(","); - - return Arrays.stream(groups) - .filter(group -> group.contains("premium-")) - .findFirst(); - } - - public static boolean isSetupAlreadyDone(@NonNull SharedPreferences preferences) { - return preferences.getBoolean(AppConstants.SETUP_COMPLETED, false); - } - - public static boolean isAccountPresent(@NonNull Context context) { - AccountManager accountManager = AccountManager.get(context.getApplicationContext()); - Account[] accountList = accountManager.getAccountsByType(context.getString(R.string.eelo_account_type)); - return accountList != null && accountList.length > 0; - } -} diff --git a/app/src/main/java/foundation/e/drive/utils/AppConstants.kt b/app/src/main/java/foundation/e/drive/utils/AppConstants.kt index f3721e603596a662ec46eb6bc5444160002f82bc..99423f99e47a32418fad864c2810e812a0f5ceb1 100644 --- a/app/src/main/java/foundation/e/drive/utils/AppConstants.kt +++ b/app/src/main/java/foundation/e/drive/utils/AppConstants.kt @@ -38,7 +38,7 @@ object AppConstants { const val notificationChannelID = "foundation.e.drive" const val WORK_GENERIC_TAG = "eDrive" const val WORK_SETUP_TAG = "eDrive-init" - const val CORRUPTED_TIMESTAMP_IN_SECOND = 4294967295L + const val CORRUPTED_TIMESTAMP_IN_MILLISECOND = 4294967295000L @JvmField val USER_AGENT = "eos(" + buildTime + ")-eDrive(" + BuildConfig.VERSION_NAME + ")" diff --git a/app/src/main/java/foundation/e/drive/utils/CommonUtils.java b/app/src/main/java/foundation/e/drive/utils/CommonUtils.java index a9fd0c00e519b24d65f0f3e41dc179d78e0d7ea4..5a605ef421e355aaa95756fd564609983eb7b31d 100644 --- a/app/src/main/java/foundation/e/drive/utils/CommonUtils.java +++ b/app/src/main/java/foundation/e/drive/utils/CommonUtils.java @@ -1,6 +1,6 @@ /* * Copyright © CLEUS SAS 2018-2019. - * Copyright © ECORP SAS 2022-2023. + * Copyright © MURENA SAS 2022-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 @@ -14,7 +14,6 @@ import android.accounts.AccountManager; import android.annotation.SuppressLint; import android.app.NotificationChannel; import android.app.NotificationManager; -import android.app.Service; import android.content.ClipData; import android.content.ClipboardManager; import android.content.ContentResolver; @@ -31,9 +30,6 @@ import java.text.StringCharacterIterator; import java.util.Locale; import foundation.e.drive.R; -import foundation.e.drive.account.AccountUserInfoWorker; -import foundation.e.drive.work.WorkRequestFactory; -import timber.log.Timber; import static foundation.e.drive.utils.AppConstants.MEDIA_SYNC_PROVIDER_AUTHORITY; import static foundation.e.drive.utils.AppConstants.METERED_NETWORK_ALLOWED_AUTHORITY; @@ -41,10 +37,6 @@ import static foundation.e.drive.utils.AppConstants.SETTINGS_SYNC_PROVIDER_AUTHO import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.work.ExistingPeriodicWorkPolicy; -import androidx.work.PeriodicWorkRequest; -import androidx.work.WorkManager; - /** * @author Vincent Bourgmayer @@ -54,41 +46,6 @@ import androidx.work.WorkManager; */ public abstract class CommonUtils { - /** - * Set ServiceUncaughtExceptionHandler to be the MainThread Exception Handler - * Or update the service which use it - * todo: check if the ServiceExceptionHandler could be remove - * @param service current service - */ - public static void setServiceUnCaughtExceptionHandler(@NonNull Service service) { - Thread.UncaughtExceptionHandler defaultUEH = Thread.getDefaultUncaughtExceptionHandler(); - if (defaultUEH != null && ServiceExceptionHandler.class.getSimpleName().equals(defaultUEH.getClass().getSimpleName())) { - ((ServiceExceptionHandler) defaultUEH).setService(service); - } else { - Thread.setDefaultUncaughtExceptionHandler(new ServiceExceptionHandler(service)); - } - } - - - /** - * This method retrieve Account corresponding to account's name and type - * - * @param accountName Account Name, shouldn't be null - * @param accountType account type - * @param am Account Manager - * @return Account or null if not found - */ - @Nullable - public static Account getAccount(@NonNull String accountName, @NonNull String accountType, @NonNull AccountManager am) { - Account[] accounts = am.getAccountsByType(accountType); - for (int i = -1, size = accounts.length; ++i < size; ) { - if (accounts[i].name.equals(accountName)) { - return accounts[i]; - } - } - return null; - } - /** * This method retrieve Account corresponding to account's type * @@ -148,11 +105,7 @@ public abstract class CommonUtils { public static boolean isMeteredNetworkAllowed(@NonNull Account account) { return ContentResolver.getSyncAutomatically(account, METERED_NETWORK_ALLOWED_AUTHORITY); } - - /* methods relative to file */ - - - + /** * Tell if there is internet connection * @@ -161,7 +114,6 @@ public abstract class CommonUtils { * @return True if there is connection, false either */ public static boolean haveNetworkConnection(@NonNull Context context, boolean meteredNetworkAllowed) { - Timber.v("haveNetworkConnection()"); final ConnectivityManager cm = context.getSystemService(ConnectivityManager.class); final NetworkCapabilities capabilities = cm.getNetworkCapabilities(cm.getActiveNetwork()); @@ -217,17 +169,6 @@ public abstract class CommonUtils { Toast.LENGTH_SHORT).show(); } - - /** - * Job for Widget & notification about quota - * - * @param workManager component used to register worker - */ - public static void registerPeriodicUserInfoChecking(@NonNull WorkManager workManager) { - final PeriodicWorkRequest workRequest = WorkRequestFactory.getPeriodicWorkRequest(WorkRequestFactory.WorkType.PERIODIC_USER_INFO); - workManager.enqueueUniquePeriodicWork(AccountUserInfoWorker.UNIQUE_WORK_NAME, ExistingPeriodicWorkPolicy.UPDATE, workRequest); - } - /** * Function for get build props through reflection * @@ -247,4 +188,4 @@ public abstract class CommonUtils { } return value == null ? "" : value; } -} +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/drive/utils/FileUtils.kt b/app/src/main/java/foundation/e/drive/utils/FileUtils.kt index 77185ebdc069cb47c985c1ea22c6883b43e92aa3..2a7c87eeb09da59355285e21f1f77632452239cf 100644 --- a/app/src/main/java/foundation/e/drive/utils/FileUtils.kt +++ b/app/src/main/java/foundation/e/drive/utils/FileUtils.kt @@ -66,4 +66,10 @@ object FileUtils { return "*/*" } } + + @JvmStatic + fun isPartFile(file: File) = file.name.endsWith(".part") + + @JvmStatic + fun isNotPartFile(file: File) = !isPartFile(file) } \ No newline at end of file diff --git a/app/src/main/java/foundation/e/drive/utils/WorkerUtils.kt b/app/src/main/java/foundation/e/drive/utils/WorkerUtils.kt deleted file mode 100644 index 8dcd6c59be9749405a3e59b9a78d23b582d64948..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/drive/utils/WorkerUtils.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright © MURENA SAS 2024. - * 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.utils - -import android.content.Context -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkManager -import foundation.e.drive.models.SyncedFolder -import foundation.e.drive.work.WorkRequestFactory - -object WorkerUtils { - fun registerSetupWorkers(context: Context) { - val rootFolderSetupWorkers = generateRootFolderSetupWorkers(context) - if (rootFolderSetupWorkers.isEmpty()) { - return - } - - val getUserInfoRequest = WorkRequestFactory.getOneTimeWorkRequest( - WorkRequestFactory.WorkType.ONE_TIME_USER_INFO, - null - ) - val finishSetupRequest = WorkRequestFactory.getOneTimeWorkRequest( - WorkRequestFactory.WorkType.ONE_TIME_FINISH_SETUP, - null - ) - - var workContinuation = WorkManager.getInstance(context) - .beginWith(getUserInfoRequest) - - rootFolderSetupWorkers.forEach { - workContinuation = workContinuation.then(it) - } - - workContinuation - .then(finishSetupRequest) - .enqueue() - } - - private fun generateRootFolderSetupWorkers(context: Context): List { - val rootSyncedFolderList: List = - RootSyncedFolderProvider.getSyncedFolderRoots(context) - - return rootSyncedFolderList.map { - WorkRequestFactory.getOneTimeWorkRequest( - WorkRequestFactory.WorkType.ONE_TIME_ROOT_FOLDER_SETUP, - it - ) - } - } -} diff --git a/app/src/main/java/foundation/e/drive/widgets/EDriveWidget.java b/app/src/main/java/foundation/e/drive/widgets/EDriveWidget.java index a839ee119e10ded258f970951362f730d3c332bf..4f2cc638110e256b32a33767e2cd6ed3c79693db 100644 --- a/app/src/main/java/foundation/e/drive/widgets/EDriveWidget.java +++ b/app/src/main/java/foundation/e/drive/widgets/EDriveWidget.java @@ -39,7 +39,7 @@ import java.util.Calendar; import java.util.Locale; import foundation.e.drive.R; -import foundation.e.drive.utils.AccountUtils; +import foundation.e.drive.account.AccountUtils; import foundation.e.drive.utils.AppConstants; import foundation.e.drive.utils.CommonUtils; import foundation.e.drive.utils.AccessTokenProvider; @@ -258,10 +258,11 @@ public class EDriveWidget extends AppWidgetProvider { views.setTextViewText(R.id.planName, context.getString(R.string.free_plan, totalShownQuota)); - AccountUtils.getPremiumGroup(accountManager, account) - .ifPresent(group -> views.setTextViewText(R.id.planName, - context.getString(R.string.premium_plan, - group.split("-")[1]))); + final String premiumPlan = AccountUtils.getPremiumPlan(accountManager, account); + if (premiumPlan != null) { + views.setTextViewText(R.id.planName, + context.getString(R.string.premium_plan, premiumPlan)); + } views.setTextViewText(R.id.status, context.getString(R.string.progress_status, usedShownQuota, totalShownQuota)); diff --git a/app/src/main/java/foundation/e/drive/work/WorkLauncher.kt b/app/src/main/java/foundation/e/drive/work/WorkLauncher.kt new file mode 100644 index 0000000000000000000000000000000000000000..21027cb9c999928fc4ccbb789967592c82158924 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/work/WorkLauncher.kt @@ -0,0 +1,127 @@ +/* + * Copyright © MURENA SAS 2023-2024. + * 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.work + +import android.content.Context +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import foundation.e.drive.account.AccountUserInfoWorker +import foundation.e.drive.models.SyncedFolder +import foundation.e.drive.periodicScan.FullScanWorker +import foundation.e.drive.periodicScan.PeriodicScanWorker +import foundation.e.drive.synchronization.SyncWorker +import foundation.e.drive.utils.RootSyncedFolderProvider + +/** + * @author Vincent Bourgmayer + */ +class WorkLauncher private constructor(context: Context) { + private val workManager: WorkManager + + init { + workManager = WorkManager.getInstance(context) + } + + companion object { + private var instance: WorkLauncher? = null + + @JvmStatic + fun getInstance(context: Context): WorkLauncher { + return instance ?: WorkLauncher(context.applicationContext) + } + } + + fun enqueuePeriodicUserInfoFetching() { + val request = WorkRequestFactory.createPeriodicWorkRequest(PeriodicWorkType.FETCH_USER_INFO) + workManager.enqueueUniquePeriodicWork( + AccountUserInfoWorker.UNIQUE_WORK_NAME, + ExistingPeriodicWorkPolicy.UPDATE, + request + ) + } + + fun enqueuePeriodicFullScan() { + val request = WorkRequestFactory.createPeriodicWorkRequest(PeriodicWorkType.PERIODIC_SCAN) + workManager.enqueueUniquePeriodicWork(PeriodicScanWorker.UNIQUE_WORK_NAME, + ExistingPeriodicWorkPolicy.KEEP, + request) + } + + fun enqueueOneTimeFullScan(isForced: Boolean) { + if (isForced) { + val request = WorkRequestFactory.createOneTimeWorkRequest(OneTimeWorkType.USER_TRIGGERED_FULL_SCAN, + null) + workManager.enqueueUniqueWork(FullScanWorker.UNIQUE_WORK_NAME, + ExistingWorkPolicy.REPLACE, + request) + return + } + + val request = WorkRequestFactory.createOneTimeWorkRequest(OneTimeWorkType.FULL_SCAN, + null) + workManager.enqueueUniqueWork(FullScanWorker.UNIQUE_WORK_NAME, + ExistingWorkPolicy.KEEP, + request) + } + + fun enqueueOneTimeAppListGenerator() { + val request = WorkRequestFactory.createOneTimeWorkRequest(OneTimeWorkType.LIST_APPS, + null) + workManager.enqueue(request) + } + + fun enqueueOneTimeSync() { + val request = WorkRequestFactory.createOneTimeWorkRequest(OneTimeWorkType.ONE_TIME_SYNC, null) + workManager.enqueueUniqueWork(SyncWorker.UNIQUE_WORK_NAME, ExistingWorkPolicy.KEEP, request) + } + + + fun enqueueSetupWorkers(context: Context): Boolean { + val rootFolderSetupWorkers = generateRootFolderSetupWorkers(context) + if (rootFolderSetupWorkers.isEmpty()) { + return false + } + + val getUserInfoRequest = WorkRequestFactory.createOneTimeWorkRequest( + OneTimeWorkType.FETCH_USER_INFO, + null + ) + + val finishSetupRequest = WorkRequestFactory.createOneTimeWorkRequest( + OneTimeWorkType.FINISH_SETUP, + null + ) + + var workContinuation = WorkManager.getInstance(context) + .beginWith(getUserInfoRequest) + + rootFolderSetupWorkers.forEach { + workContinuation = workContinuation.then(it) + } + + workContinuation + .then(finishSetupRequest) + .enqueue() + + return true + } + + private fun generateRootFolderSetupWorkers(context: Context): List { + val rootSyncedFolderList: List = + RootSyncedFolderProvider.getSyncedFolderRoots(context) + + return rootSyncedFolderList.map { + WorkRequestFactory.createOneTimeWorkRequest( + OneTimeWorkType.ROOT_FOLDER_SETUP, + it + ) + } + } +} diff --git a/app/src/main/java/foundation/e/drive/work/WorkRequestFactory.java b/app/src/main/java/foundation/e/drive/work/WorkRequestFactory.java deleted file mode 100644 index 460ed5761fb8714b00e86b32080691a92bba748b..0000000000000000000000000000000000000000 --- a/app/src/main/java/foundation/e/drive/work/WorkRequestFactory.java +++ /dev/null @@ -1,280 +0,0 @@ -/* - * Copyright © MURENA SAS 2022-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.work; - -import static androidx.work.BackoffPolicy.LINEAR; -import static java.util.concurrent.TimeUnit.MINUTES; -import static foundation.e.drive.account.setup.RootFolderSetupWorker.DATA_KEY_ENABLE; -import static foundation.e.drive.account.setup.RootFolderSetupWorker.DATA_KEY_ID; -import static foundation.e.drive.account.setup.RootFolderSetupWorker.DATA_KEY_LAST_ETAG; -import static foundation.e.drive.account.setup.RootFolderSetupWorker.DATA_KEY_LAST_MODIFIED; -import static foundation.e.drive.account.setup.RootFolderSetupWorker.DATA_KEY_LIBELLE; -import static foundation.e.drive.account.setup.RootFolderSetupWorker.DATA_KEY_LOCAL_PATH; -import static foundation.e.drive.account.setup.RootFolderSetupWorker.DATA_KEY_MEDIATYPE; -import static foundation.e.drive.account.setup.RootFolderSetupWorker.DATA_KEY_REMOTE_PATH; -import static foundation.e.drive.account.setup.RootFolderSetupWorker.DATA_KEY_SCAN_LOCAL; -import static foundation.e.drive.account.setup.RootFolderSetupWorker.DATA_KEY_SCAN_REMOTE; -import static foundation.e.drive.utils.AppConstants.WORK_GENERIC_TAG; -import static foundation.e.drive.utils.AppConstants.WORK_SETUP_TAG; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.work.Constraints; -import androidx.work.Data; -import androidx.work.NetworkType; -import androidx.work.OneTimeWorkRequest; -import androidx.work.PeriodicWorkRequest; - -import java.security.InvalidParameterException; - -import foundation.e.drive.account.AccountUserInfoWorker; -import foundation.e.drive.account.setup.FinishSetupWorker; -import foundation.e.drive.account.setup.RootFolderSetupWorker; -import foundation.e.drive.models.SyncedFolder; -import foundation.e.drive.periodicScan.FullScanWorker; -import foundation.e.drive.periodicScan.ListAppsWorker; -import foundation.e.drive.periodicScan.PeriodicScanWorker; -import foundation.e.drive.synchronization.SyncWorker; - -public class WorkRequestFactory { - public enum WorkType { - PERIODIC_USER_INFO, - PERIODIC_SCAN, - ONE_TIME_FULL_SCAN, - ONE_TIME_FORCED_FULL_SCAN, - ONE_TIME_APP_LIST, - ONE_TIME_USER_INFO, - ONE_TIME_ROOT_FOLDER_SETUP, - ONE_TIME_FINISH_SETUP, - ONE_TIME_SYNC - } - - private final static int PERIODIC_WORK_REPEAT_INTERVAL = 30; - private final static int PERIODIC_SCAN_FLEX_TIME = 5; - - /** - * Build an instance of PeriodicWorkRequest depending of the work type specified - * @param type WorkType. Should be FULL_SCAN or PERIODIC_USER_INFO or PERIODIC_APP_LIST - * If not, it will throw an InvalidParameterException - * @return Periodic WorkRequest - */ - @NonNull - public static PeriodicWorkRequest getPeriodicWorkRequest(@NonNull WorkType type) { - switch (type) { - case PERIODIC_SCAN: - return createPeriodicScanWorkRequest(); - case PERIODIC_USER_INFO: - return createPeriodicGetUserInfoWorkRequest(); - default: - throw new InvalidParameterException("Unsupported Work Type: " + type); - } - } - - /** - * Create a PeriodicWorkRequest instance for - * a Full scan with constraints on network (should - * be unmetered) and battery (shouldn't be low) - * @return instance of PeriodicWorkRequest - */ - @NonNull - private static PeriodicWorkRequest createPeriodicScanWorkRequest() { - final Constraints constraints = createUnmeteredNetworkAndHighBatteryConstraints(); - - final PeriodicWorkRequest.Builder workRequestBuilder = new PeriodicWorkRequest.Builder( - PeriodicScanWorker.class, - PERIODIC_WORK_REPEAT_INTERVAL, MINUTES, - PERIODIC_SCAN_FLEX_TIME, MINUTES); - - return workRequestBuilder.setConstraints(constraints) - .addTag(WORK_GENERIC_TAG) - .build(); - } - - /** - * Create a periodic work request to get userInfo - * @return instance of PeriodicWorkRequest - */ - @NonNull - private static PeriodicWorkRequest createPeriodicGetUserInfoWorkRequest() { - final Constraints constraints = new Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .build(); - - final PeriodicWorkRequest.Builder workRequestBuilder = new PeriodicWorkRequest.Builder(AccountUserInfoWorker.class, - PERIODIC_WORK_REPEAT_INTERVAL, MINUTES); - - return workRequestBuilder.addTag(WORK_GENERIC_TAG) - .setConstraints(constraints) - .build(); - } - - /** - * Build an instance of OneTimeWorkRequest depending of the work type specified. - * @param type Should be ONE_TIME_USER_INFO, or FIRST_START, or CREATE_REMOTE_DIR - * or it will throw InvalidParameterException - * @param syncedFolder this parameter is required for CREATE_REMOTE_DIR work type. If null it will throw an NPE. - * @return OneTimeWorkRequest's instance. - */ - @NonNull - public static OneTimeWorkRequest getOneTimeWorkRequest(@NonNull WorkType type, @Nullable SyncedFolder syncedFolder) { - switch (type) { - case ONE_TIME_APP_LIST: - return createAppListGenerationWorkRequest(); - case ONE_TIME_FULL_SCAN: - return createFullScanWorkRequest(false); - case ONE_TIME_FORCED_FULL_SCAN: - return createFullScanWorkRequest(true); - case ONE_TIME_USER_INFO: - return createGetUserInfoWorkRequest(); - case ONE_TIME_FINISH_SETUP: - return createFinishSetupWorkRequest(); - case ONE_TIME_ROOT_FOLDER_SETUP: - if (syncedFolder == null) throw new NullPointerException("Synced folder is null"); - return createRootFolderSetupWorkRequest(syncedFolder); - case ONE_TIME_SYNC: - return createSyncWorkRequest(); - default: - throw new InvalidParameterException("Unsupported Work Type: " + type); - } - } - - private static OneTimeWorkRequest createSyncWorkRequest() { - final Constraints constraints = new Constraints.Builder() - .setRequiredNetworkType(NetworkType.CONNECTED) - .setRequiresBatteryNotLow(true) - .build(); - - final OneTimeWorkRequest.Builder builder = new OneTimeWorkRequest.Builder(SyncWorker.class); - - return builder.setBackoffCriteria(LINEAR, 2, MINUTES).addTag(WORK_GENERIC_TAG) - .setConstraints(constraints) - .build(); - } - - /** - * Create a workRequest to generate file which contains list of installed apps - * @return the workRequest - */ - @NonNull - private static OneTimeWorkRequest createAppListGenerationWorkRequest() { - final OneTimeWorkRequest.Builder builder = new OneTimeWorkRequest.Builder(ListAppsWorker.class); - - return builder.setBackoffCriteria(LINEAR, 2, MINUTES) - .addTag(WORK_GENERIC_TAG) - .build(); - } - - /** - * Create a OneTimeWorkRequest instance for - * a Full scan with constraints on network (should - * be unmetered) and battery (shouldn't be low) - * @return instance of OneTimeWorkRequest - */ - @NonNull - private static OneTimeWorkRequest createFullScanWorkRequest(boolean forced) { - final Constraints constraints = createUnmeteredNetworkAndHighBatteryConstraints(); - - final OneTimeWorkRequest.Builder builder = new OneTimeWorkRequest.Builder(FullScanWorker.class); - - final Data data = new Data.Builder() - .putBoolean(FullScanWorker.ACTION_FORCED_SYNC_KEY, forced) - .build(); - - return builder.setBackoffCriteria(LINEAR, 2, MINUTES) - .setConstraints(constraints) - .setInputData(data) - .addTag(WORK_GENERIC_TAG) - .build(); - } - - - /** - * Instanciate a OneTimeWorkRequest to retrieve user info - * @return instance of OneTimeWorkRequest - */ - @NonNull - private static OneTimeWorkRequest createGetUserInfoWorkRequest() { - final Constraints constraints = createUnmeteredNetworkAndHighBatteryConstraints(); - - final OneTimeWorkRequest.Builder builder = new OneTimeWorkRequest.Builder(AccountUserInfoWorker.class); - - return builder.setBackoffCriteria(LINEAR, 2, MINUTES) - .addTag(WORK_GENERIC_TAG) - .addTag(WORK_SETUP_TAG) - .setConstraints(constraints) - .build(); - } - - /** - * Create a OneTime workRequest to create a remote folder - * With constraints on network (unmetered) and battery (not low) - * @param syncedFolder SyncedFolder instance with data about folder to create - * @return Instance OneTimeWorkRequest - */ - @NonNull - private static OneTimeWorkRequest createRootFolderSetupWorkRequest(@NonNull SyncedFolder syncedFolder) { - final Constraints constraints = createUnmeteredNetworkAndHighBatteryConstraints(); - - return new OneTimeWorkRequest.Builder( - RootFolderSetupWorker.class) - .setBackoffCriteria(LINEAR, 2, MINUTES) - .setInputData(createDataFromSyncedFolder(syncedFolder)) - .addTag(WORK_GENERIC_TAG) - .addTag(WORK_SETUP_TAG) - .setConstraints(constraints) - .build(); - } - - /** - * Create a OneTime WorkRequest which finish setup process - * @return Instance of OneTimeWorkRequest - */ - @NonNull - private static OneTimeWorkRequest createFinishSetupWorkRequest() { - return new OneTimeWorkRequest.Builder(FinishSetupWorker.class) - .setBackoffCriteria(LINEAR, 2, MINUTES) - .addTag(WORK_GENERIC_TAG) - .addTag(WORK_SETUP_TAG) - .build(); - } - - /** - * Create Constraints for unmetered network - * and battery not low - * @return instance of Constraints - */ - @NonNull - private static Constraints createUnmeteredNetworkAndHighBatteryConstraints() { - return new Constraints.Builder() - .setRequiredNetworkType(NetworkType.UNMETERED) - .setRequiresBatteryNotLow(true) - .build(); - } - - /** - * Parse SyncedFolder instance in Data, used as data for WorkRequest - * @param folder SyncedFolder instance - * @return Data instance - */ - @NonNull - private static Data createDataFromSyncedFolder(@NonNull SyncedFolder folder) { - return new Data.Builder() - .putInt(DATA_KEY_ID, folder.getId()) - .putString(DATA_KEY_LIBELLE, folder.getLibelle()) - .putString(DATA_KEY_LOCAL_PATH, folder.getLocalFolder()) - .putString(DATA_KEY_REMOTE_PATH, folder.getRemoteFolder()) - .putString(DATA_KEY_LAST_ETAG, folder.getLastEtag()) - .putLong(DATA_KEY_LAST_MODIFIED, folder.getLastModified()) - .putBoolean(DATA_KEY_SCAN_LOCAL, folder.isScanLocal()) - .putBoolean(DATA_KEY_SCAN_REMOTE, folder.isScanRemote()) - .putBoolean(DATA_KEY_ENABLE, folder.isEnabled()) - .putBoolean(DATA_KEY_MEDIATYPE, folder.isMediaType()) - .build(); - } -} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/drive/work/WorkRequestFactory.kt b/app/src/main/java/foundation/e/drive/work/WorkRequestFactory.kt new file mode 100644 index 0000000000000000000000000000000000000000..0c3b245b806627a42bfbce23fb1c4a1949c6e064 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/work/WorkRequestFactory.kt @@ -0,0 +1,247 @@ +/* + * Copyright © MURENA SAS 2022-2024. + * 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.work + +import androidx.work.BackoffPolicy.LINEAR +import androidx.work.Constraints +import androidx.work.Data +import androidx.work.NetworkType +import androidx.work.OneTimeWorkRequest +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.PeriodicWorkRequest +import androidx.work.PeriodicWorkRequestBuilder +import foundation.e.drive.account.AccountUserInfoWorker +import foundation.e.drive.account.setup.FinishSetupWorker +import foundation.e.drive.account.setup.RootFolderSetupWorker +import foundation.e.drive.account.setup.RootFolderSetupWorker.DATA_KEY_ENABLE +import foundation.e.drive.account.setup.RootFolderSetupWorker.DATA_KEY_ID +import foundation.e.drive.account.setup.RootFolderSetupWorker.DATA_KEY_LAST_ETAG +import foundation.e.drive.account.setup.RootFolderSetupWorker.DATA_KEY_LAST_MODIFIED +import foundation.e.drive.account.setup.RootFolderSetupWorker.DATA_KEY_LIBELLE +import foundation.e.drive.account.setup.RootFolderSetupWorker.DATA_KEY_LOCAL_PATH +import foundation.e.drive.account.setup.RootFolderSetupWorker.DATA_KEY_MEDIATYPE +import foundation.e.drive.account.setup.RootFolderSetupWorker.DATA_KEY_REMOTE_PATH +import foundation.e.drive.account.setup.RootFolderSetupWorker.DATA_KEY_SCAN_LOCAL +import foundation.e.drive.account.setup.RootFolderSetupWorker.DATA_KEY_SCAN_REMOTE +import foundation.e.drive.models.SyncedFolder +import foundation.e.drive.periodicScan.FullScanWorker +import foundation.e.drive.periodicScan.ListAppsWorker +import foundation.e.drive.periodicScan.PeriodicScanWorker +import foundation.e.drive.synchronization.SyncWorker +import foundation.e.drive.utils.AppConstants.WORK_GENERIC_TAG +import foundation.e.drive.utils.AppConstants.WORK_SETUP_TAG +import java.util.concurrent.TimeUnit.MINUTES + + +enum class PeriodicWorkType { + FETCH_USER_INFO, + PERIODIC_SCAN} + +enum class OneTimeWorkType { + FULL_SCAN, + USER_TRIGGERED_FULL_SCAN, + LIST_APPS, + FETCH_USER_INFO, + ROOT_FOLDER_SETUP, + FINISH_SETUP, + ONE_TIME_SYNC +} + +object WorkRequestFactory { + + private const val PERIODIC_WORK_REPEAT_INTERVAL_IN_MIN = 30L + private const val PERIODIC_SCAN_FLEX_TIME_IN_MIN = 5L + + /** + * Builds an instance of PeriodicWorkRequest depending of the work type specified + * @param type WorkType. Should be PERIODIC_SCAN or PERIODIC_USER_INFO + * @return Periodic WorkRequest + */ + fun createPeriodicWorkRequest(type: PeriodicWorkType): PeriodicWorkRequest { + return when (type) { + PeriodicWorkType.FETCH_USER_INFO -> createPeriodicGetUserInfoWorkRequest() + PeriodicWorkType.PERIODIC_SCAN -> createPeriodicScanWorkRequest() + } + } + + /** + * Builds an instance of OneTimeWorkRequest depending of the work type specified. + * @param type Should be ONE_TIME_USER_INFO, or FIRST_START, or CREATE_REMOTE_DIR + * or it will throw InvalidParameterException + * @param syncedFolder @Nullable this parameter is required for CREATE_REMOTE_DIR work type. Throw an NPE if null. + * @return OneTimeWorkRequest's instance. + */ + fun createOneTimeWorkRequest(type: OneTimeWorkType, syncedFolder: SyncedFolder? = null): OneTimeWorkRequest { + return when (type) { + OneTimeWorkType.FULL_SCAN -> createFullScanWorkRequest(false) + OneTimeWorkType.USER_TRIGGERED_FULL_SCAN -> createFullScanWorkRequest(true) + OneTimeWorkType.LIST_APPS -> createAppListGenerationWorkRequest() + OneTimeWorkType.FETCH_USER_INFO -> createGetUserInfoWorkRequest() + OneTimeWorkType.ROOT_FOLDER_SETUP -> { + if (syncedFolder == null) { + throw NullPointerException("Cannot create RootFolderSetupWorker without syncFolder") + } + createRootFolderSetupWorkRequest(syncedFolder) + } + OneTimeWorkType.FINISH_SETUP -> createFinishSetupWorkRequest() + OneTimeWorkType.ONE_TIME_SYNC -> createSyncWorkRequest() + } + } + + /** + * Creates a OneTimeWorkRequest instance for + * a Full scan with constraints on network (CONNECTED) and battery (shouldn't be low) + * @return instance of OneTimeWorkRequest + */ + private fun createFullScanWorkRequest(forced: Boolean): OneTimeWorkRequest { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresBatteryNotLow(true) + .build() + + val data = Data.Builder() + .putBoolean(FullScanWorker.ACTION_FORCED_SYNC_KEY, forced) + .build() + + return OneTimeWorkRequestBuilder() + .setBackoffCriteria(LINEAR, 2, MINUTES) + .setConstraints(constraints) + .setInputData(data) + .addTag(WORK_GENERIC_TAG) + .build() + } + + /** + * Creates a workRequest to generate file which contains list of installed apps + * @return the workRequest + */ + private fun createAppListGenerationWorkRequest(): OneTimeWorkRequest { + return OneTimeWorkRequestBuilder() + .setBackoffCriteria(LINEAR, 2, MINUTES) + .addTag(WORK_GENERIC_TAG) + .build() + + } + + /** + * Instantiates a OneTimeWorkRequest to retrieve user info + * @return instance of OneTimeWorkRequest + */ + private fun createGetUserInfoWorkRequest(): OneTimeWorkRequest { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresBatteryNotLow(true) + .build() + + return OneTimeWorkRequestBuilder() + .setConstraints(constraints) + .setBackoffCriteria(LINEAR, 2, MINUTES) + .addTag(WORK_GENERIC_TAG) + .addTag(WORK_SETUP_TAG) + .build() + } + + /** + * Creates a OneTime workRequest to create a remote folder + * With constraints on network (unmetered) and battery (not low) + * @param syncedFolder SyncedFolder instance with data about folder to create + * @return Instance OneTimeWorkRequest + */ + private fun createRootFolderSetupWorkRequest(syncedFolder: SyncedFolder): OneTimeWorkRequest { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.UNMETERED) + .setRequiresBatteryNotLow(true) + .build() + + return OneTimeWorkRequestBuilder() + .setInputData(createDataFromSyncedFolder(syncedFolder)) + .setConstraints(constraints) + .setBackoffCriteria(LINEAR, 2, MINUTES) + .addTag(WORK_GENERIC_TAG) + .addTag(WORK_SETUP_TAG) + .build() + } + + /** + * Parses SyncedFolder instance in Data, used as data for WorkRequest + * @param folder SyncedFolder instance + * @return Data instance + */ + private fun createDataFromSyncedFolder(folder: SyncedFolder): Data { + return Data.Builder() + .putInt(DATA_KEY_ID, folder.getId()) + .putString(DATA_KEY_LIBELLE, folder.getLibelle()) + .putString(DATA_KEY_LOCAL_PATH, folder.getLocalFolder()) + .putString(DATA_KEY_REMOTE_PATH, folder.getRemoteFolder()) + .putString(DATA_KEY_LAST_ETAG, folder.getLastEtag()) + .putLong(DATA_KEY_LAST_MODIFIED, folder.getLastModified()) + .putBoolean(DATA_KEY_SCAN_LOCAL, folder.isScanLocal()) + .putBoolean(DATA_KEY_SCAN_REMOTE, folder.isScanRemote()) + .putBoolean(DATA_KEY_ENABLE, folder.isEnabled()) + .putBoolean(DATA_KEY_MEDIATYPE, folder.isMediaType()) + .build() + + } + + /** + * Creates a OneTime WorkRequest which finish setup process + * @return Instance of OneTimeWorkRequest + */ + private fun createFinishSetupWorkRequest(): OneTimeWorkRequest { + return OneTimeWorkRequestBuilder() + .setBackoffCriteria(LINEAR, 2, MINUTES) + .addTag(WORK_GENERIC_TAG) + .addTag(WORK_SETUP_TAG) + .build() + + } + + private fun createSyncWorkRequest(): OneTimeWorkRequest { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresBatteryNotLow(true) + .build() + + return OneTimeWorkRequestBuilder() + .setBackoffCriteria(LINEAR, 2, MINUTES) + .setConstraints(constraints) + .addTag(WORK_GENERIC_TAG) + .build() + } + + /** + * Creates a PeriodicWorkRequest instance for + * a Full scan with constraints on network (CONNECTED) and battery (not low) + * @return instance of PeriodicWorkRequest + */ + private fun createPeriodicScanWorkRequest(): PeriodicWorkRequest { + val constraints = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .setRequiresBatteryNotLow(true) + .build() + + return PeriodicWorkRequestBuilder( + PERIODIC_WORK_REPEAT_INTERVAL_IN_MIN, MINUTES, + PERIODIC_SCAN_FLEX_TIME_IN_MIN,MINUTES) + .setConstraints(constraints) + .addTag(WORK_GENERIC_TAG) + .build() + } + + private fun createPeriodicGetUserInfoWorkRequest(): PeriodicWorkRequest { + val constraint = Constraints.Builder() + .setRequiredNetworkType(NetworkType.CONNECTED) + .build() + + return PeriodicWorkRequestBuilder( + PERIODIC_WORK_REPEAT_INTERVAL_IN_MIN, MINUTES) + .setConstraints(constraint) + .addTag(WORK_GENERIC_TAG) + .build() + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fda41306fe16f9966de42198a8c4576956b5c720..1ebbd1a25159a0a0efefca494dc111b8152bd573 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -31,7 +31,7 @@ 99% of your allotted cloud storage is used. Please take action. You\'ve filled your allotted cloud storage up to 90%. You\'ve filled your allotted cloud storage up to 80%. - File synchronization in progress + File synchronization %d file to sync %d files to sync diff --git a/app/src/test/java/foundation/e/drive/operations/UploadFileOperationTest.java b/app/src/test/java/foundation/e/drive/operations/UploadFileOperationTest.java index 8bcb28ef1ebabab01c4339723c4e83d2576b33ee..cb4adbf13e0baf128255101db342f91065df7800 100644 --- a/app/src/test/java/foundation/e/drive/operations/UploadFileOperationTest.java +++ b/app/src/test/java/foundation/e/drive/operations/UploadFileOperationTest.java @@ -323,26 +323,22 @@ public class UploadFileOperationTest { @Test public void formatTimeStampToMatchCloud_validTimestamp() { final long initialTimestamp = 1683017095074L; - final String expectedOutput = "1683017095"; + final long expectedOutput = 1683017095L; final SyncedFileState mockedSyncedState = Mockito.mock(SyncedFileState.class); final UploadFileOperation operationUnderTest = new UploadFileOperation(mockedSyncedState, account, context); - final String output = operationUnderTest.formatTimestampToMatchCloud(initialTimestamp); - assertNotNull("Output shouldn't be null", output); - assertFalse("Output shouldn't not be empty.", output.isEmpty()); + final long output = operationUnderTest.formatTimestampToMatchCloud(initialTimestamp); assertEquals("Output doesn't match expectation.", expectedOutput, output); } @Test public void formatTimeStampToMatchCloud_invalidTimestamp() { - final long initialTimestamp = 0; - final String expectedOutput = "0"; + final long initialTimestamp = 0L; + final long expectedOutput = 0L; final SyncedFileState mockedSyncedState = Mockito.mock(SyncedFileState.class); final UploadFileOperation operationUnderTest = new UploadFileOperation(mockedSyncedState, account, context); - final String output = operationUnderTest.formatTimestampToMatchCloud(initialTimestamp); - assertNotNull("Output shouldn't be null", output); - assertFalse("Output shouldn't not be empty.", output.isEmpty()); + final long output = operationUnderTest.formatTimestampToMatchCloud(initialTimestamp); assertEquals("Output doesn't match expectation.", expectedOutput, output); } diff --git a/app/src/test/java/foundation/e/drive/periodicScan/contentScanner/FileDiffUtilsTest.kt b/app/src/test/java/foundation/e/drive/periodicScan/contentScanner/FileDiffUtilsTest.kt index f078df1d2bc0eef1bbd7e5df11527f216460ab4a..f94fdebf65c5e4a0199301cc851d7dc2a9aaf8d8 100644 --- a/app/src/test/java/foundation/e/drive/periodicScan/contentScanner/FileDiffUtilsTest.kt +++ b/app/src/test/java/foundation/e/drive/periodicScan/contentScanner/FileDiffUtilsTest.kt @@ -10,6 +10,7 @@ package foundation.e.drive.periodicScan.contentScanner import com.owncloud.android.lib.resources.files.model.RemoteFile import foundation.e.drive.models.SyncedFileState import foundation.e.drive.periodicScan.contentScanner.FileDiffUtils +import foundation.e.drive.utils.AppConstants import org.junit.Assert import org.junit.Test import org.mockito.Mockito @@ -294,21 +295,21 @@ internal class FileDiffUtilsTest { /* isCorruptedTimestamp for localFile */ @Test fun `isCorruptedTimestamp() return true with timestamp equal to max of Int32 `() { - val corruptedTimestamp = 4294967295L + val corruptedTimestamp = AppConstants.CORRUPTED_TIMESTAMP_IN_MILLISECOND val resultUnderTest = FileDiffUtils.isCorruptedTimestamp(corruptedTimestamp) - Assert.assertTrue("isCorruptedTimestamp(4294967295L) returned $resultUnderTest instead of true", resultUnderTest) + Assert.assertTrue("isCorruptedTimestamp(4294967295000L) returned $resultUnderTest instead of true", resultUnderTest) } @Test fun `isCorruptedTimestamp() return true with timestamp bigger than max of Int32 `() { - val corruptedTimestamp = 4294967295L + 1 + val corruptedTimestamp = AppConstants.CORRUPTED_TIMESTAMP_IN_MILLISECOND + 1 val resultUnderTest = FileDiffUtils.isCorruptedTimestamp(corruptedTimestamp) - Assert.assertTrue("isCorruptedTimestamp(4294967295L) returned $resultUnderTest instead of true", resultUnderTest) + Assert.assertTrue("isCorruptedTimestamp(4294967295000L) returned $resultUnderTest instead of true", resultUnderTest) } @Test fun `isCorruptedTimestamp() return false with timestamp smaller than max of Int32 `() { - val corruptedTimestamp = 4294967295L - 1 + val corruptedTimestamp = AppConstants.CORRUPTED_TIMESTAMP_IN_MILLISECOND - 1 val resultUnderTest = FileDiffUtils.isCorruptedTimestamp(corruptedTimestamp) Assert.assertFalse("isCorruptedTimestamp(4294967295000L) returned $resultUnderTest instead of false", resultUnderTest) } diff --git a/detekt.yml b/detekt.yml index 4efb86519050178026690426c89a3500c666d82e..fb4a8a38b4f8325fe2ee8f75206fe62fafd3e84f 100644 --- a/detekt.yml +++ b/detekt.yml @@ -14,6 +14,9 @@ style: ForbiddenComment: active: false + ReturnCount: + excludeGuardClauses: true + # Complexity rules complexity: diff --git a/dev_tools.sh b/dev_tools.sh new file mode 100755 index 0000000000000000000000000000000000000000..442f5de7ede0d63f9e6e0d3ff3a425aba969cbab --- /dev/null +++ b/dev_tools.sh @@ -0,0 +1,120 @@ + +# Author Vincent Bourgmayer +# 2023 November 22nd + +function printHelp() { + echo "Utilisation: ./eDrive_dev_tools.sh [opt] [(optionnal:) value] + Note: This script strongly rely on ADB, so it won't work if ADB is not installed + + Options list: + -a : add Account into the device + -c : clear eDrive's data + -C : clear adb logcat + -d : dump database (generate a database dumb on the device. The dump is reachable by the user) + -f : Trigger fullScan work (scan cloud & device then try to sync changed files) + -h : print this help + -L : (required value: true|false) : true: enable/ false: disable full log + -l : (option: --clear) : --clear: clear logcat before. Otherwise just display logcat. Default: false + -n : Start nextcloud docker instance + -o : (required value: true|false) true: enable File Observer / false: disable File Observer + -p : Pull database directory from device into current computer folder + -s : test sync : Create 10 empty files in 'Document' folder and try to upload them + "; +} + + +function registerAccount() { + echo "calling script to add account into device + + /"'!'"\\ Device need to be connecter to WI_FI + + /"'!'"\\ In order to let it work, + check that credentials & server URL + are set in registerAccount2.sh + + /i\\ please also consider, that the script will need to be updated if UI + changes + "; + + + ./registerAccount.sh +} + +function setFullLog() { + if [ -z "$1" ]; then + echo "The parameter -L require a value (true|false)" + elif [ "$1" != "true" ] && [ "$1" != "false" ]; then + echo "Invalid parameter: $1. You need to provide: (true|false)" + else + echo "Sending broacast with $1 as parameter" + adb shell am broadcast -a foundation.e.drive.action.FULL_LOG_ON_PROD --receiver-include-background --ez full_log_enable "$1" + fi +} + +function displayLogcat() { + adb logcat --pid=$(adb shell pidof -s foundation.e.drive) +} + +function clearLogcat() { + adb logcat --clear +} + +function pullDatabase() { + adb pull /data/data/foundation.e.drive/databases +} + +function clearAppData() { + echo "clear eDrive's data"; + adb shell pm clear foundation.e.drive +} + +function dumpDatabase() { + echo "Dumping database" + adb shell am broadcast -a foundation.e.drive.action.DUMP_DATABASE --receiver-include-background +} + +function forceFullScan() { + adb shell am broadcast -a foundation.e.drive.action.FORCE_SCAN --receiver-include-background +} + +function testSync() { + adb shell am broadcast -a foundation.e.drive.action.TEST_SYNC --receiver-include-background +} + +function startNCDocker() { + docker run -d -p 8080:80 -e NEXTCLOUD_TRUSTED_DOMAINS=":8080" -e SQLITE_DATABASE=nc -e NEXTCLOUD_ADMIN_USER=admin -e NEXTCLOUD_ADMIN_PASSWORD=admin nextcloud:26 +} + +function enableFileObserver() { + if [ -z "$1" ]; then + echo "The parameter -L require a value (true|false)" + elif [ "$1" != "true" ] && [ "$1" != "false" ]; then + echo "Invalid parameter: $1. You need to provide: (true|false)" + else + echo "Sending broacast with $1 as parameter" + adb shell am broadcast -a foundation.e.drive.action.ENABLE_FILE_OBSERVER --receiver-include-background --ez file_observer_enable "$1" + fi +} + + +while getopts acCdfhL:lnops flag +do + case "${flag}" in + a) registerAccount;; + c) clearAppData;; + C) clearLogcat;; + d) dumpDatabase;; + f) forceFullScan;; + h) printHelp;; + l) displayLogcat;; + L) setFullLog ${OPTARG};; + n) startNCDocker;; + o) enableFileObserver ${OPTARG};; + p) pullDatabase;; + s) testSync;; + esac +done + +echo "|----FINISHED----|" +exit 0 + diff --git a/registerAccount.sh b/registerAccount.sh new file mode 100755 index 0000000000000000000000000000000000000000..438228d150f6e3d55d61da9e78a2c7f11b3eecd1 --- /dev/null +++ b/registerAccount.sh @@ -0,0 +1,55 @@ + +# Open Add account settings page (doesn't work if already opened and not closed) +adb shell am start -a android.settings.ADD_ACCOUNT_SETTINGS + +sleep 1 +# Select /e/ account +# number of input depend on language set on the device + +adb shell input keyevent TAB +sleep 1 + +# Open the /e/ account : add account form +adb shell input keyevent ENTER + + +# CREDENTIALS & SERVER URL + +MAIL="PUT YOUR VALUE" +PWD="PUT YOUR VALUE" +SERVER_URI="PUT YOUR VALUE" + +sleep 1 +#Insert Login +adb shell input text $MAIL +adb shell input keyevent TAB + +sleep 1 +#Insert PWD +adb shell input text $PWD + +## IF NO server URI +if [ -z $SERVER_URI ] +then + adb shell input keyevent TAB + adb shell input keyevent ENTER +else + + # Move across element + adb shell input keyevent TAB + adb shell input keyevent TAB + adb shell input keyevent TAB + + # Enter server URI + adb shell input keyevent ENTER + adb shell input keyevent TAB + adb shell input text $SERVER_URI + + # Move again to login btn + adb shell input keyevent TAB + adb shell input keyevent TAB + adb shell input keyevent TAB + adb shell input keyevent TAB + adb shell input keyevent TAB + adb shell input keyevent ENTER +fi