diff --git a/app/build.gradle b/app/build.gradle index ca4b9865d2f616d05908dc97740709a98d4a8514..9ed8b30b7a3e0d3fe4fd14db50182b205a016947 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -94,7 +94,7 @@ android { } dependencies { - implementation 'com.github.nextcloud:android-library:2.13.0' + implementation 'foundation.e:Nextcloud-Android-Library:1.0.5-release' implementation "commons-httpclient:commons-httpclient:3.1@jar" implementation fileTree(include: ['*.jar'], dir: 'libs') api 'androidx.annotation:annotation:1.6.0' diff --git a/app/src/main/java/foundation/e/drive/account/AccountUserInfoWorker.java b/app/src/main/java/foundation/e/drive/account/AccountUserInfoWorker.java index ef376d0cde5dc847009ed4a9d1ef8108b21e0040..c1361c95b86d3d5b3900b4811f5fb23320883c9f 100644 --- a/app/src/main/java/foundation/e/drive/account/AccountUserInfoWorker.java +++ b/app/src/main/java/foundation/e/drive/account/AccountUserInfoWorker.java @@ -88,6 +88,7 @@ public class AccountUserInfoWorker extends Worker { .diskCacheStrategy(DiskCacheStrategy.ALL) .preload(); ViewUtils.updateWidgetView(mContext); + DavClientProvider.getInstance().saveAccounts(mContext); return Result.success(); } else { return Result.retry(); @@ -235,6 +236,9 @@ public class AccountUserInfoWorker extends Worker { } accountManager.setUserData(account, ACCOUNT_DATA_ALIAS_KEY, aliases); Timber.d("fetchAliases(): success"); + + DavClientProvider.getInstance().saveAccounts(mContext); + return true; } } 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 a95d15c63d37382700ab3b2b8e93bc25941550c1..0459bf3b9680349cd530509b6a8501c95114de06 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 @@ -16,6 +16,7 @@ import foundation.e.drive.R import foundation.e.drive.utils.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 timber.log.Timber @@ -44,6 +45,7 @@ class AccountAddedReceiver() : BroadcastReceiver() { .putString(AccountManager.KEY_ACCOUNT_NAME, accountName) .apply() + DavClientProvider.getInstance().cleanUp(); WorkerUtils.registerSetupWorkers(context) } @@ -73,7 +75,7 @@ class AccountAddedReceiver() : BroadcastReceiver() { } if (!isExistingAccount(accountName, accountType, context)) { - Timber.w("No account exist for username: %s ", accountType, accountName) + Timber.w("No account exist for type: %s, username: %s", accountType, accountName) return false } return true diff --git a/app/src/main/java/foundation/e/drive/account/receivers/AccountRemoveCallbackReceiver.java b/app/src/main/java/foundation/e/drive/account/receivers/AccountRemoveCallbackReceiver.java index a04a6b185f3737e57f06a02393395af43c5c13b7..4ff1ed1ef1f24ec0d7156511b9d66f0387ac3431 100644 --- a/app/src/main/java/foundation/e/drive/account/receivers/AccountRemoveCallbackReceiver.java +++ b/app/src/main/java/foundation/e/drive/account/receivers/AccountRemoveCallbackReceiver.java @@ -13,6 +13,7 @@ import static foundation.e.drive.utils.AppConstants.SETUP_COMPLETED; import android.accounts.AccountManager; import android.annotation.SuppressLint; +import android.app.Application; import android.app.NotificationManager; import android.content.BroadcastReceiver; import android.content.Context; @@ -25,11 +26,10 @@ import androidx.work.WorkManager; import java.io.File; -import foundation.e.drive.EdriveApplication; import foundation.e.drive.R; import foundation.e.drive.database.DbHelper; import foundation.e.drive.database.FailedSyncPrefsManager; -import foundation.e.drive.synchronization.SyncWorker; +import foundation.e.drive.synchronization.SyncProxy; import foundation.e.drive.utils.AppConstants; import foundation.e.drive.utils.DavClientProvider; import foundation.e.drive.utils.ViewUtils; @@ -51,11 +51,12 @@ public class AccountRemoveCallbackReceiver extends BroadcastReceiver { } cancelWorkers(applicationContext); - stopRecursiveFileObserver(applicationContext); + SyncProxy.INSTANCE.moveToIdle((Application) applicationContext); deleteDatabase(applicationContext); cleanSharedPreferences(applicationContext, preferences); removeCachedFiles(applicationContext); deleteNotificationChannels(applicationContext); + DavClientProvider.getInstance().cleanUp(); ViewUtils.updateWidgetView(applicationContext); @@ -72,12 +73,6 @@ public class AccountRemoveCallbackReceiver extends BroadcastReceiver { Timber.d("Remove Database: %s", result); } - private void stopRecursiveFileObserver(@NonNull Context applicationContext) { - if (applicationContext instanceof EdriveApplication) { - ((EdriveApplication) applicationContext).stopRecursiveFileObserver(); - } - } - private boolean shouldProceedWithRemoval(@NonNull Intent intent, @NonNull SharedPreferences preferences, @NonNull Context context) { if (isInvalidAction(intent) || intent.getExtras() == null) { Timber.w("Invalid account removal request"); 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 6488eeb54c74b1c487105bf1090d945cf72f8d29..b028653c990dbf6937b3e2f9fb0ce55f37b2ac37 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 @@ -94,6 +94,9 @@ public class RootFolderSetupWorker extends Worker { new CreateFolderRemoteOperation(syncedFolder.getRemoteFolder(), true); @SuppressWarnings("deprecation") final RemoteOperationResult result = mkcolRequest.execute(client); + + DavClientProvider.getInstance().saveAccounts(context); + if (result.isSuccess() || result.getCode() == RemoteOperationResult.ResultCode.FOLDER_ALREADY_EXISTS) { DbHelper.insertSyncedFolder(syncedFolder, context); return Result.success(); 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 5929f3709885d120e5dbd7b2226bcb5b0ce0fb46..641b3c47a498ca4060d997c4fa454ce6e856bc7e 100644 --- a/app/src/main/java/foundation/e/drive/activity/AccountsActivity.java +++ b/app/src/main/java/foundation/e/drive/activity/AccountsActivity.java @@ -166,4 +166,10 @@ public class AccountsActivity extends AppCompatActivity { binding.avatar.setVisibility(View.VISIBLE); } } -} \ No newline at end of file + + @Override + protected void onDestroy() { + DavClientProvider.getInstance().saveAccounts(this); + super.onDestroy(); + } +} 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 49cc9c533e850a707300423842e9db091c528273..08415ed91aa8c9bfddb0264109a8b0fc9a3f93dd 100644 --- a/app/src/main/java/foundation/e/drive/periodicScan/FullScanWorker.kt +++ b/app/src/main/java/foundation/e/drive/periodicScan/FullScanWorker.kt @@ -191,6 +191,8 @@ class FullScanWorker(private val context: Context, private val workerParams: Wor val syncedFoldersIds = listRemoteFilesOperation.syncedFoldersId val syncedFileStates = DbHelper.getSyncedFileStatesByFolders(context, syncedFoldersIds) + DavClientProvider.getInstance().saveAccounts(applicationContext) + if (remoteFiles.isNotEmpty() || syncedFileStates.isNotEmpty()) { val scanner = RemoteContentScanner(context, syncedFolders) return scanner.scanContent(remoteFiles, syncedFileStates) diff --git a/app/src/main/java/foundation/e/drive/synchronization/StateMachine.kt b/app/src/main/java/foundation/e/drive/synchronization/StateMachine.kt index 7be44b40581a84f2e08921783777455a4f8f853d..90f923b9801ce5d395e8125152b50b1d255bedeb 100644 --- a/app/src/main/java/foundation/e/drive/synchronization/StateMachine.kt +++ b/app/src/main/java/foundation/e/drive/synchronization/StateMachine.kt @@ -22,7 +22,7 @@ enum class SyncState { * It can be synchronizing (transfering file) * It can be listening for file event * It can be Scanning cloud & device for missed files - * It can be Idle (i.e: after boot, before restart) + * It can be Idle (i.e: after boot, before restart, after logout) * @author Vincent Bourgmayer */ object StateMachine { @@ -41,7 +41,7 @@ object StateMachine { SyncState.PERIODIC_SCAN -> setPeriodicScanState() SyncState.SYNCHRONIZING -> setSynchronizing() SyncState.LISTENING_FILES -> setListeningFilesState() - SyncState.IDLE -> false + SyncState.IDLE -> setIdleFilesState() } if (!isStateChanged) { @@ -59,6 +59,11 @@ object StateMachine { return true } + private fun setIdleFilesState(): Boolean { + currentState = SyncState.IDLE + return true + } + private fun setPeriodicScanState(): Boolean { if (currentState == SyncState.SYNCHRONIZING) { Timber.d("Cannot change state: files sync is running") 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 ef829bda64d950871f3351b844bf423f914da960..568e37f65ca499fa6e422a8dda72eee127f7850b 100644 --- a/app/src/main/java/foundation/e/drive/synchronization/SyncProxy.kt +++ b/app/src/main/java/foundation/e/drive/synchronization/SyncProxy.kt @@ -31,6 +31,7 @@ interface SyncRequestCollector { fun startSynchronization(context: Context) fun onPeriodicScanStart(application: Application): Boolean fun startListeningFiles(application: Application) + fun moveToIdle(application: Application) } /** @@ -180,6 +181,25 @@ object SyncProxy: SyncRequestCollector, SyncManager { } } + /* + * called after account logged out + * update the stateMachine's state & stop recursive fileObserver + */ + override fun moveToIdle(application: Application) { + if (application !is EdriveApplication) { + Timber.d("Invalid parameter: : moveToIdle(application)") + return + } + + val isStateChanged = StateMachine.changeState(SyncState.IDLE) + if (!isStateChanged) { + Timber.d("failed to change state. moveToIdle") + return + } + + application.stopRecursiveFileObserver() + } + /** * Progressively delay synchronization of a file in case of failure 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 eda410b26725e604426f23f46127c62c2224f48e..2eaa32fcdaeef09f4bbc5bf9e6d5b6555c330eb8 100644 --- a/app/src/main/java/foundation/e/drive/synchronization/SyncWorker.kt +++ b/app/src/main/java/foundation/e/drive/synchronization/SyncWorker.kt @@ -80,6 +80,8 @@ class SyncWorker( notificationManager.cancel(NOTIFICATION_ID) syncManager.startListeningFiles(applicationContext as EdriveApplication) + + DavClientProvider.getInstance().saveAccounts(applicationContext) } catch (exception: Exception) { Timber.w(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 e141198c227332893d809b1071251254de005a99..5f5ec8593eea6734103223db30f0f626079b3fb4 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 @@ -165,6 +165,8 @@ public class UploadFileOperation extends RemoteOperation { } final RemoteOperationResult checkQuotaResult = checkAvailableSpace(ncClient, file.length()); + DavClientProvider.getInstance().saveAccounts(context); + if (checkQuotaResult.getCode() != ResultCode.OK) { Timber.d("Impossible to check quota. Cancels upload of %s", syncedState.getLocalPath()); return checkQuotaResult.getCode(); @@ -264,7 +266,7 @@ public class UploadFileOperation extends RemoteOperation { @VisibleForTesting() @NonNull public RemoteOperationResult uploadChunkedFile(@NonNull final File file, @NonNull final OwnCloudClient client) { - final String timeStamp = formatTimestampToMatchCloud(file.lastModified()); + final long timeStamp = file.lastModified() / 1000; final String mimeType = getMimeType(file); final ChunkedFileUploadRemoteOperation uploadOperation = new ChunkedFileUploadRemoteOperation(syncedState.getLocalPath(), syncedState.getRemotePath(), @@ -321,7 +323,7 @@ public class UploadFileOperation extends RemoteOperation { @VisibleForTesting() @NonNull public RemoteOperationResult uploadFile(@NonNull final File file, @NonNull final OwnCloudClient client, boolean checkEtag) { - final String timeStamp = formatTimestampToMatchCloud(file.lastModified()); + final long timeStamp = file.lastModified() / 1000; final String eTag = checkEtag ? syncedState.getLastEtag() : null; final UploadFileRemoteOperation uploadOperation = new UploadFileRemoteOperation(syncedState.getLocalPath(), diff --git a/app/src/main/java/foundation/e/drive/utils/DavClientProvider.java b/app/src/main/java/foundation/e/drive/utils/DavClientProvider.java index fc8ce0006978369363b6952b06b885b1142b9d3f..842db6658efa2e984c910404889c2b4f135ea8ef 100644 --- a/app/src/main/java/foundation/e/drive/utils/DavClientProvider.java +++ b/app/src/main/java/foundation/e/drive/utils/DavClientProvider.java @@ -11,124 +11,86 @@ package foundation.e.drive.utils; import android.accounts.Account; -import android.accounts.AccountManager; +import android.accounts.AuthenticatorException; +import android.accounts.OperationCanceledException; import android.content.Context; -import android.net.Uri; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.nextcloud.common.NextcloudClient; -import com.nextcloud.common.OkHttpCredentialsUtil; -import com.owncloud.android.lib.common.OwnCloudBasicCredentials; +import com.owncloud.android.lib.common.OwnCloudAccount; import com.owncloud.android.lib.common.OwnCloudClient; -import com.owncloud.android.lib.common.OwnCloudClientFactory; +import com.owncloud.android.lib.common.OwnCloudClientManager; import com.owncloud.android.lib.common.OwnCloudClientManagerFactory; import com.owncloud.android.lib.common.accounts.AccountUtils; +import java.io.IOException; + +import foundation.e.drive.R; import timber.log.Timber; /** * @author Vincent Bourgmayer */ -@SuppressWarnings({"deprecation", "DeprecatedIsStillUsed"}) +@SuppressWarnings({"deprecation"}) public class DavClientProvider { private final static DavClientProvider instance = new DavClientProvider(); private DavClientProvider() { - Timber.tag(DavClientProvider.class.getSimpleName()); } - @Deprecated - private OwnCloudClient ocClientInstance; - private NextcloudClient ncClientInstance; - @Nullable public OwnCloudClient getClientInstance(@Nullable final Account account, @NonNull final Context context) { - if (account == null) return null; - - if (ocClientInstance == null) { - ocClientInstance = createOcClient(account, context); + if (account == null) { + return null; } - return ocClientInstance; - } - - - @Nullable - public NextcloudClient getNcClientInstance(@Nullable final Account account, @NonNull final Context context) { - if (account == null) return null; + OwnCloudClientManagerFactory.setUserAgent(AppConstants.USER_AGENT); - if (ncClientInstance == null) { - ncClientInstance = createNcClient(account, context); + try { + final OwnCloudClientManager ownCloudClientManager = OwnCloudClientManagerFactory.getDefaultSingleton(); + final OwnCloudAccount ocAccount = new OwnCloudAccount(account, context); + return ownCloudClientManager.getClientFor(ocAccount, context); + } catch (AccountUtils.AccountNotFoundException | OperationCanceledException | + AuthenticatorException | IOException exception) { + Timber.e(exception); + return null; } - return ncClientInstance; - } - - public void cleanUp() { - ncClientInstance = null; - ocClientInstance = null; - } - - @NonNull - public static DavClientProvider getInstance() { - return instance; } @Nullable - private OwnCloudBasicCredentials getOcCredentials(@NonNull Account account, @NonNull Context context) throws AccountUtils.AccountNotFoundException { - final String pwd = AccountManager.get(context).getPassword(account); - if (pwd == null) return null; - return new OwnCloudBasicCredentials(account.name, pwd); - } + public NextcloudClient getNcClientInstance(@Nullable final Account account, @NonNull final Context context) { + if (account == null) { + return null; + } - @Nullable - private OwnCloudClient createOcClient(@NonNull Account account, @NonNull Context context) { OwnCloudClientManagerFactory.setUserAgent(AppConstants.USER_AGENT); - final OwnCloudClient result; - try { - final OwnCloudBasicCredentials credentials = getOcCredentials(account, context); - if (credentials == null) return null; - final Uri serverUri = Uri.parse(AccountUtils.getBaseUrlForAccount(context, account)); - result = OwnCloudClientFactory.createOwnCloudClient(serverUri, context, true); - result.setCredentials(credentials); - } catch (AccountUtils.AccountNotFoundException exception) { + try { + final OwnCloudClientManager ownCloudClientManager = OwnCloudClientManagerFactory.getDefaultSingleton(); + final OwnCloudAccount ocAccount = new OwnCloudAccount(account, context); + return ownCloudClientManager.getNextcloudClientFor(ocAccount, context); + } catch (AccountUtils.AccountNotFoundException | OperationCanceledException | + AuthenticatorException | IOException exception) { Timber.e(exception); return null; } - if (result.getUserId() == null) { - final String userId = AccountManager.get(context).getUserData(account, AppConstants.ACCOUNT_USER_ID_KEY); - result.setUserId(userId); - } - - return result; } - - @Nullable - private String getNcCredentials(@NonNull Account account, @NonNull Context context) throws AccountUtils.AccountNotFoundException { - final String pwd = AccountManager.get(context).getPassword(account); - if (pwd == null) return null; - return OkHttpCredentialsUtil.basic(account.name, pwd); + public void cleanUp() { + final OwnCloudClientManager ownCloudClientManager = OwnCloudClientManagerFactory.getDefaultSingleton(); + ownCloudClientManager.removeClientForByName(null); } - @Nullable - private NextcloudClient createNcClient(@NonNull Account account, @NonNull Context context) { - final NextcloudClient result; - try { - final String credentials = getNcCredentials(account, context); - if (credentials == null) return null; + public void saveAccounts(@NonNull Context context) { + OwnCloudClientManagerFactory.getDefaultSingleton().saveAllClients(context, context.getString(R.string.eelo_account_type)); + } - final Uri serverUri = Uri.parse(AccountUtils.getBaseUrlForAccount(context, account)); - result = OwnCloudClientFactory.createNextcloudClient(serverUri, account.name, credentials, context, true); - } catch (AccountUtils.AccountNotFoundException exception) { - Timber.e("Can't get server URI for account: %s\n%s", account.name, exception.getMessage()); - return null; - } - result.setUserId(account.name); - return result; + @NonNull + public static DavClientProvider getInstance() { + return instance; } -} \ 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 index 84a48585339b68ebd8cb1f4507c8d3e00b3a9744..8dcd6c59be9749405a3e59b9a78d23b582d64948 100644 --- a/app/src/main/java/foundation/e/drive/utils/WorkerUtils.kt +++ b/app/src/main/java/foundation/e/drive/utils/WorkerUtils.kt @@ -15,9 +15,7 @@ import foundation.e.drive.models.SyncedFolder import foundation.e.drive.work.WorkRequestFactory object WorkerUtils { - fun registerSetupWorkers(context: Context) { - DavClientProvider.getInstance().cleanUp() - + fun registerSetupWorkers(context: Context) { val rootFolderSetupWorkers = generateRootFolderSetupWorkers(context) if (rootFolderSetupWorkers.isEmpty()) { return @@ -32,9 +30,14 @@ object WorkerUtils { null ) - WorkManager.getInstance(context) + var workContinuation = WorkManager.getInstance(context) .beginWith(getUserInfoRequest) - .then(rootFolderSetupWorkers) + + rootFolderSetupWorkers.forEach { + workContinuation = workContinuation.then(it) + } + + workContinuation .then(finishSetupRequest) .enqueue() } diff --git a/app/src/test/java/foundation/e/drive/synchronization/StateMachineTest.kt b/app/src/test/java/foundation/e/drive/synchronization/StateMachineTest.kt index c1674fca0c2c14533afa3ad6430cb53bed1f1cd9..a4a765dfdab1b74f6a81675154e10388a3757828 100644 --- a/app/src/test/java/foundation/e/drive/synchronization/StateMachineTest.kt +++ b/app/src/test/java/foundation/e/drive/synchronization/StateMachineTest.kt @@ -50,14 +50,14 @@ class StateMachineTest { } @Test - fun `Changing State from PERIODIC_SCAN to IDLE should return false`() { + fun `Changing State from PERIODIC_SCAN to IDLE should return true`() { StateMachine.currentState = PERIODIC_SCAN val result = StateMachine.changeState(IDLE) - Assert.assertFalse("Changing state from PERIODIC_SCAN to IDLE returned false", result) + Assert.assertTrue("Changing state from PERIODIC_SCAN to IDLE returned false", result) val currentState = StateMachine.currentState - Assert.assertEquals("Current state should remain PERIODIC_SCAN but is $currentState", PERIODIC_SCAN, currentState) + Assert.assertEquals("Current state should be IDLE but is $currentState", IDLE, currentState) } @Test @@ -83,14 +83,14 @@ class StateMachineTest { } @Test - fun `Changing State from LISTENING_FILE to IDLE should return false`() { + fun `Changing State from LISTENING_FILE to IDLE should return true`() { StateMachine.currentState = LISTENING_FILES val result = StateMachine.changeState(IDLE) - Assert.assertFalse("Changing state from LISTENING_FILES to IDLE returned true", result) + Assert.assertTrue("Changing state from LISTENING_FILES to IDLE returned false", result) val currentState = StateMachine.currentState - Assert.assertEquals("Current state should remain LISTENING_FILES but is $currentState", LISTENING_FILES, currentState) + Assert.assertEquals("Current state should be IDLE but is $currentState", IDLE, currentState) } @Test @@ -138,14 +138,14 @@ class StateMachineTest { } @Test - fun `Changing State from SYNCHRONIZING to IDLE should return false`() { + fun `Changing State from SYNCHRONIZING to IDLE should return true`() { StateMachine.currentState = SYNCHRONIZING val result = StateMachine.changeState(IDLE) - Assert.assertFalse("Changing state from SYNCHRONIZING to IDLE returned true", result) + Assert.assertTrue("Changing state from SYNCHRONIZING to IDLE returned false", result) val currentState = StateMachine.currentState - Assert.assertEquals("Current state should remain SYNCHRONIZING but is $currentState", SYNCHRONIZING, currentState) + Assert.assertEquals("Current state should be IDLE but is $currentState", IDLE, currentState) } @Test