diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f8c0bfe3a27bf3c8676e74696719bd83647883ca..ac42e944d90f00c4a785e052e7b300a75e101e9f 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -83,7 +83,6 @@
android:exported="true"
android:label="@string/account_setting_metered_network"
tools:ignore="ExportedContentProvider" />
-
+
+
\ 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
index aa621e6fc590bb97a8890f8c3807d37d848d2ce7..1d5a9c506f6142e8c19ac9f35ce16460bf67ed48 100644
--- a/app/src/main/java/foundation/e/drive/FileObservers/FileEventListener.java
+++ b/app/src/main/java/foundation/e/drive/FileObservers/FileEventListener.java
@@ -105,12 +105,12 @@ public class FileEventListener {
* @param request SyncRequest that should be executed asap
*/
private void sendSyncRequestToSynchronizationService(@NonNull SyncRequest request) {
- Timber.d("Sending a SyncRequest for %s", request.getSyncedFileState().getName());
-
final SyncRequestCollector syncManager = SyncProxy.INSTANCE;
- syncManager.queueSyncRequest(request, appContext.getApplicationContext());
- syncManager.startSynchronization(appContext);
-
+ final boolean requestAdded = syncManager.queueSyncRequest(request, appContext.getApplicationContext());
+ if (requestAdded) {
+ Timber.d("Sending a SyncRequest for %s", request.getSyncedFileState().getName());
+ syncManager.startSynchronization(appContext);
+ }
}
/**
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 bcfd6b5e6b3588b39fbaa487de87f7e811b3f985..668fb93d9f05257c6d556e2a18d91038e868b64c 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.NotificationManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@@ -20,13 +21,15 @@ import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+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.services.SynchronizationService;
+import foundation.e.drive.synchronization.SyncWorker;
import foundation.e.drive.utils.AppConstants;
import foundation.e.drive.utils.DavClientProvider;
import foundation.e.drive.utils.ViewUtils;
@@ -43,21 +46,27 @@ public class AccountRemoveCallbackReceiver extends BroadcastReceiver {
final SharedPreferences preferences = applicationContext.getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE);
- if (!shouldProceedWithRemoval(intent, preferences)) {
+ if (!shouldProceedWithRemoval(intent, preferences, applicationContext)) {
return;
}
+ cancelWorkers(applicationContext);
stopRecursiveFileObserver(applicationContext);
- stopAllServices(applicationContext);
deleteDatabase(applicationContext);
cleanSharedPreferences(applicationContext, preferences);
removeCachedFiles(applicationContext);
-
+ deleteNotificationChannels(applicationContext);
DavClientProvider.getInstance().cleanUp();
ViewUtils.updateWidgetView(applicationContext);
}
+
+ private void cancelWorkers(@NonNull Context context) {
+ final WorkManager workManager = WorkManager.getInstance(context);
+ workManager.cancelAllWorkByTag(AppConstants.WORK_GENERIC_TAG);
+ }
+
private void deleteDatabase(@NonNull Context applicationContext) {
final boolean result = applicationContext.deleteDatabase(DbHelper.DATABASE_NAME);
Timber.d("Remove Database: %s", result);
@@ -69,16 +78,16 @@ public class AccountRemoveCallbackReceiver extends BroadcastReceiver {
}
}
- private boolean shouldProceedWithRemoval(@NonNull Intent intent, @NonNull SharedPreferences preferences) {
+ private boolean shouldProceedWithRemoval(@NonNull Intent intent, @NonNull SharedPreferences preferences, @NonNull Context context) {
if (isInvalidAction(intent) || intent.getExtras() == null) {
Timber.w("Invalid account removal request");
return false;
}
String currentAccount = preferences.getString(AccountManager.KEY_ACCOUNT_NAME, "");
- String currentAccountType = preferences.getString(AccountManager.KEY_ACCOUNT_TYPE, "");
+ String currentAccountType = context.getString(R.string.eelo_account_type);
- if (currentAccount.isEmpty() || currentAccountType.isEmpty()) {
+ if (currentAccount.isEmpty()) {
Timber.d("No account set up, ignoring account removal");
return false;
}
@@ -93,13 +102,6 @@ public class AccountRemoveCallbackReceiver extends BroadcastReceiver {
return !"android.accounts.action.ACCOUNT_REMOVED".equals(intent.getAction());
}
- private void stopAllServices(@NonNull Context applicationContext) {
-
- Intent synchronizationServiceIntent = new Intent(applicationContext, SynchronizationService.class);
- boolean syncServiceStopResult = applicationContext.stopService(synchronizationServiceIntent);
- Timber.d("stop SynchronizationService: %s", syncServiceStopResult);
- }
-
private void cleanSharedPreferences(@NonNull Context applicationContext, @NonNull SharedPreferences prefs) {
if (!applicationContext.deleteSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME)) {
//If removal failed, clear all data inside
@@ -144,4 +146,16 @@ public class AccountRemoveCallbackReceiver extends BroadcastReceiver {
return dir.delete();
}
+
+ private void deleteNotificationChannels(Context context) {
+ NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
+
+ notificationManager.cancelAll();
+ try {
+ notificationManager.deleteNotificationChannel(AppConstants.notificationChannelID);
+ notificationManager.deleteNotificationChannel(SyncWorker.NOTIF_CHANNEL_ID);
+ } catch (Exception exception) {
+ Timber.e(exception, "Cannot delete notification Channel");
+ }
+ }
}
diff --git a/app/src/main/java/foundation/e/drive/models/SyncWrapper.java b/app/src/main/java/foundation/e/drive/models/SyncWrapper.java
index 6c73855a6c39ae2f18630e8c099537c2b5727151..6bbf6221f3ac5a56ce963b8e4b08540a9220883d 100644
--- a/app/src/main/java/foundation/e/drive/models/SyncWrapper.java
+++ b/app/src/main/java/foundation/e/drive/models/SyncWrapper.java
@@ -15,8 +15,8 @@ import androidx.annotation.Nullable;
import com.owncloud.android.lib.common.operations.RemoteOperation;
-import foundation.e.drive.operations.DownloadFileOperation;
-import foundation.e.drive.operations.UploadFileOperation;
+import foundation.e.drive.synchronization.tasks.DownloadFileOperation;
+import foundation.e.drive.synchronization.tasks.UploadFileOperation;
/**
* This class encapsulates data, for SynchronizationService, about a thread which run a RemoteOperation
@@ -26,7 +26,6 @@ import foundation.e.drive.operations.UploadFileOperation;
public class SyncWrapper {
private final SyncRequest request;
private final RemoteOperation remoteOperation;
- private boolean isRunning;
/**
* Build an instance of SyncThreadHolder for a file transfer
@@ -36,7 +35,6 @@ public class SyncWrapper {
public SyncWrapper(@NonNull final SyncRequest request, @Nullable final Account account, @NonNull final Context context) {
this.request = request;
remoteOperation = createRemoteOperation(request, account, context);
- isRunning = true;
}
@Nullable
@@ -44,14 +42,6 @@ public class SyncWrapper {
return remoteOperation;
}
- public boolean isRunning() {
- return isRunning;
- }
-
- public synchronized void setRunning(boolean running) {
- isRunning = running;
- }
-
/**
* Create the RemoteOperation (to perform file transfer) based on SyncRequest
* @param request syncRequest for the file
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 0a4776aec98e1fdc537fe9b6552278250523b81d..49cc9c533e850a707300423842e9db091c528273 100644
--- a/app/src/main/java/foundation/e/drive/periodicScan/FullScanWorker.kt
+++ b/app/src/main/java/foundation/e/drive/periodicScan/FullScanWorker.kt
@@ -56,7 +56,6 @@ class FullScanWorker(private val context: Context, private val workerParams: Wor
val startAllowed = checkStartConditions(account, prefs, requestCollector)
if (!startAllowed) {
Timber.d("Start Periodic Scan is not allowed")
- requestCollector.startListeningFiles(applicationContext as Application)
return Result.failure()
}
@@ -66,12 +65,12 @@ class FullScanWorker(private val context: Context, private val workerParams: Wor
return Result.success()
}
- val remoteSyncRequests = scanRemoteFiles(account, syncFolders.toMutableList())
+ val remoteSyncRequests = scanRemoteFiles(account, syncFolders)
syncRequests.putAll(remoteSyncRequests)
Timber.d("${remoteSyncRequests.size} request collected from cloud")
- val localSyncRequests = scanLocalFiles(syncFolders.toMutableList())
+ val localSyncRequests = scanLocalFiles(syncFolders)
syncRequests.putAll(localSyncRequests)
Timber.d("${localSyncRequests.size} request collected from device")
diff --git a/app/src/main/java/foundation/e/drive/services/SynchronizationService.java b/app/src/main/java/foundation/e/drive/services/SynchronizationService.java
deleted file mode 100644
index 42700dc3ebfed8f8d2b1527bb13173e1511afa43..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/drive/services/SynchronizationService.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.services;
-
-import static foundation.e.drive.operations.UploadFileOperation.FILE_SIZE_FLOOR_FOR_CHUNKED;
-
-import android.accounts.Account;
-import android.accounts.AccountManager;
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.os.Handler;
-import android.os.HandlerThread;
-import android.os.IBinder;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import com.owncloud.android.lib.common.OwnCloudClient;
-import com.owncloud.android.lib.common.operations.OnRemoteOperationListener;
-import com.owncloud.android.lib.common.operations.RemoteOperation;
-import com.owncloud.android.lib.common.operations.RemoteOperationResult;
-
-import java.io.File;
-import java.util.Map;
-
-import foundation.e.drive.R;
-import foundation.e.drive.database.DbHelper;
-import foundation.e.drive.database.FailedSyncPrefsManager;
-import foundation.e.drive.models.SyncRequest;
-import foundation.e.drive.models.SyncWrapper;
-import foundation.e.drive.models.SyncedFileState;
-import foundation.e.drive.operations.DownloadFileOperation;
-import foundation.e.drive.operations.UploadFileOperation;
-import foundation.e.drive.synchronization.SyncManager;
-import foundation.e.drive.synchronization.SyncProxy;
-import foundation.e.drive.utils.AppConstants;
-import foundation.e.drive.utils.CommonUtils;
-import foundation.e.drive.utils.DavClientProvider;
-import foundation.e.drive.work.FileSyncDisabler;
-import timber.log.Timber;
-
-/**
- * @author Vincent Bourgmayer
- */
-public class SynchronizationService extends Service implements OnRemoteOperationListener, FileSyncDisabler.FileSyncDisablingListener {
- private final SyncManager syncManager = SyncProxy.INSTANCE;
-
- private Account account;
- private final int workerAmount = 2;
- private Thread[] threadPool;
- @SuppressWarnings("DeprecatedIsStillUsed")
- @Deprecated
- private OwnCloudClient ocClient;
- private Handler handler;
- private HandlerThread handlerThread;
-
- @Override
- public void onCreate() {
- super.onCreate();
- Timber.tag(SynchronizationService.class.getSimpleName());
- }
-
- @SuppressWarnings("deprecation") //for OwnCloudClient
- @Override
- public int onStartCommand(@NonNull Intent intent, int flags, int startId) {
- Timber.v("onStartCommand()");
-
- final SharedPreferences prefs = this.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);
- account = CommonUtils.getAccount(accountName, accountType, AccountManager.get(this));
-
- if (account == null) {
- Timber.d("No account available");
- syncManager.startListeningFiles(getApplication());
- stopSelf();
- return START_NOT_STICKY;
- }
-
- threadPool = new Thread[workerAmount];
-
- ocClient = DavClientProvider.getInstance().getClientInstance(account, getApplicationContext());
- if (ocClient == null) {
- Timber.w("ocClient is null");
- }
-
- handlerThread = new HandlerThread("syncService_onResponse");
- handlerThread.start();
- handler = new Handler(handlerThread.getLooper());
-
- startSynchronization();
-
- return START_REDELIVER_INTENT;
- }
-
- @Override
- public void onDestroy() {
- if (handlerThread != null) {
- handlerThread.quitSafely();
- }
-
- super.onDestroy();
- }
-
- public void startSynchronization(){
- Timber.d("startAllThreads");
- for(int i =-1; ++i < workerAmount;){
- this.startWorker(i);
- }
- }
-
- private void startWorker(int threadIndex) {
- if (ocClient == null) {
- Timber.w("Can't start sync thread: ocClient is null");
- return;
- }
-
- if (!isNetworkAvailable()) {
- Timber.d("No network available: Clear syncRequestQueue");
- syncManager.clearRequestQueue();
- return;
- }
-
- if (!canStart(threadIndex)) {
- Timber.d("Can't start thread #%s, thread is already running", threadIndex);
- return;
- }
-
- final SyncRequest request = this.syncManager.pollSyncRequest(); //return null if empty
- if (request == null) {
- Timber.d("Thread #%s: No more sync request to start.", threadIndex);
- syncManager.removeStartedRequest(threadIndex);
- if (!syncManager.isAnySyncRequestRunning()) {
- syncManager.startListeningFiles(getApplication());
- }
- return;
- }
-
- if (!CommonUtils.isThisSyncAllowed(account, request.getSyncedFileState().isMediaType())) {
- Timber.d("thread #%s won't start cause sync is not allowed by user setting", threadIndex);
- return;
- }
-
- final SyncWrapper syncWrapper = new SyncWrapper(request, account, getApplicationContext());
- if (request.getOperationType().equals(SyncRequest.Type.DISABLE_SYNCING)) {
-
- Timber.d(" starts 'sync disabling' for file : %s on thread #%s", request.getSyncedFileState().getName(), threadIndex);
- final FileSyncDisabler fileSyncDisabler = new FileSyncDisabler(request.getSyncedFileState());
- threadPool[threadIndex] = new Thread(fileSyncDisabler.getRunnable(handler, threadIndex, getApplicationContext(), this));
- threadPool[threadIndex].start();
- syncManager.addStartedRequest(threadIndex,syncWrapper);
- return;
- }
-
- @SuppressWarnings("rawtypes")
- final RemoteOperation operation = syncWrapper.getRemoteOperation();
-
- if (operation == null ) return;
-
- CommonUtils.createNotificationChannel(this);
- Timber.v(" starts %s on thread #%s for: %s",request.getOperationType().name(),
- threadIndex, request.getSyncedFileState().getName());
-
- //noinspection deprecation
- threadPool[threadIndex] = operation.execute(ocClient, this, handler);
- syncManager.addStartedRequest(threadIndex, syncWrapper);
- }
-
- /**
- * Check if conditions are met to run a new file transfer
- * @param threadIndex index of thread on which we want to perform the transfer
- * @return false if nogo
- */
- private boolean canStart(int threadIndex) {
- final SyncWrapper syncWrapper = syncManager.getStartedRequestOnThread(threadIndex);
- return (syncWrapper == null || !syncWrapper.isRunning());
- }
-
- private boolean isNetworkAvailable() {
- final boolean meteredNetworkAllowed = CommonUtils.isMeteredNetworkAllowed(account);
- return CommonUtils.haveNetworkConnection(getApplicationContext(), meteredNetworkAllowed);
- }
-
- @Override
- public void onRemoteOperationFinish(@NonNull RemoteOperation callerOperation, @NonNull RemoteOperationResult result) {
- Timber.v("onRemoteOperationFinish()");
-
- boolean isNetworkDisconnected = false;
-
- logSyncResult(result.getCode(), callerOperation);
-
- switch (result.getCode()) {
- case UNKNOWN_ERROR:
- case FORBIDDEN:
- if (callerOperation instanceof UploadFileOperation) {
- final int rowAffected = DbHelper.forceFoldertoBeRescan(((UploadFileOperation) callerOperation).getSyncedState().getId(), getApplicationContext());
- Timber.d("Force folder to be rescan next time (row affected) : %s", rowAffected);
- }
- break;
- case NO_NETWORK_CONNECTION:
- case WRONG_CONNECTION:
- isNetworkDisconnected = true;
- break;
- }
-
- if (isNetworkDisconnected) {
- syncManager.clearRequestQueue();
- }
-
- for (Map.Entry keyValue : syncManager.getStartedRequests().entrySet()) {
- final SyncWrapper wrapper = keyValue.getValue();
- final RemoteOperation wrapperOperation = wrapper.getRemoteOperation();
- if (wrapperOperation != null && wrapperOperation.equals(callerOperation)) {
- wrapper.setRunning(false);
- updateFailureCounter(wrapper.getRequest(), result.isSuccess());
- startWorker(keyValue.getKey());
- break;
- }
- }
- }
-
- private void logSyncResult(@NonNull RemoteOperationResult.ResultCode resultCode, @NonNull RemoteOperation callerOperation) {
- String fileName;
- String operationType;
- if (callerOperation instanceof UploadFileOperation) {
- operationType = "Upload";
- fileName = ((UploadFileOperation) callerOperation).getSyncedState().getName();
- } else if (callerOperation instanceof DownloadFileOperation) {
- operationType = "Download";
- fileName = ((DownloadFileOperation) callerOperation).getRemoteFilePath();
- } else return;
- Timber.d("%s operation for %s result in: %s", operationType, fileName, resultCode.name());
- }
-
- private void updateFailureCounter(SyncRequest request, boolean success) {
- final FailedSyncPrefsManager failedPref = FailedSyncPrefsManager.getInstance(getApplicationContext());
- final SyncedFileState fileState = request.getSyncedFileState();
-
- final int fileStateId = fileState.getId();
-
- if (success) {
- failedPref.removeDataForFile(fileStateId);
- } else {
-
- if (request.getOperationType().equals(SyncRequest.Type.UPLOAD)) {
- if (fileState.getLocalPath() == null) return;
- final File file = new File(fileState.getLocalPath());
-
- if (file.length() >= FILE_SIZE_FLOOR_FOR_CHUNKED) {
- return; //do not delay future synchronization when it's for a chunked upload
- }
- }
- failedPref.saveFileSyncFailure(fileStateId);
- }
- }
-
- @Override
- public void onSyncDisabled(int threadId, boolean succeed) {
- final SyncWrapper wrapper = syncManager.getStartedRequestOnThread(threadId);
- if (wrapper != null) {
- final SyncRequest request = wrapper.getRequest();
- Timber.d("%s sync disabled ? %s", request.getSyncedFileState().getLocalPath(), succeed);
- wrapper.setRunning(false);
- updateFailureCounter(request, succeed);
- }
- startWorker(threadId);
- }
-
- @Nullable
- @Override
- public IBinder onBind(@Nullable Intent intent) {
- return null;
- }
-}
\ No newline at end of file
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 13c458e1ef2d80c3dd8eb4d2119d42ae2e77b038..ef829bda64d950871f3351b844bf423f914da960 100644
--- a/app/src/main/java/foundation/e/drive/synchronization/SyncProxy.kt
+++ b/app/src/main/java/foundation/e/drive/synchronization/SyncProxy.kt
@@ -9,12 +9,14 @@ package foundation.e.drive.synchronization
import android.app.Application
import android.content.Context
-import android.content.Intent
+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.services.SynchronizationService
+import foundation.e.drive.work.WorkRequestFactory
+import foundation.e.drive.work.WorkRequestFactory.WorkType.ONE_TIME_SYNC
import timber.log.Timber
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentLinkedQueue
@@ -24,7 +26,7 @@ import java.util.concurrent.ConcurrentLinkedQueue
* @author Vincent Bourgmayer
*/
interface SyncRequestCollector {
- fun queueSyncRequest(request: SyncRequest, context: Context)
+ fun queueSyncRequest(request: SyncRequest, context: Context): Boolean
fun queueSyncRequests(requests: MutableCollection, context: Context)
fun startSynchronization(context: Context)
fun onPeriodicScanStart(application: Application): Boolean
@@ -37,19 +39,16 @@ interface SyncRequestCollector {
*/
interface SyncManager {
fun pollSyncRequest(): SyncRequest?
- fun addStartedRequest(threadId: Int, syncWrapper: SyncWrapper)
+ fun addStartedRequest(fileLocalPath: String, syncWrapper: SyncWrapper)
fun isQueueEmpty(): Boolean
+ fun getQueueSize(): Int
fun clearRequestQueue()
- fun getStartedRequestOnThread(threadIndex: Int): SyncWrapper?
- fun getStartedRequests(): ConcurrentHashMap
- fun isAnySyncRequestRunning(): Boolean
- fun removeStartedRequest(threadIndex: Int)
+ fun removeStartedRequest(fileLocalPath: String)
fun startListeningFiles(application: Application)
}
/**
* This class goals is to act as a proxy between file's change detection & performing synchronization
- * todo 3. one improvement could be to handle file that has just been synced in order to prevent instant detection to trigger a syncRequest in reaction (i.e in case of a download)
*
* This object must allow concurrent access between (periodic | instant) file's change detection and synchronization
* it holds the SyncRequest Queue and a list of running sync
@@ -57,7 +56,7 @@ interface SyncManager {
*/
object SyncProxy: SyncRequestCollector, SyncManager {
private val syncRequestQueue: ConcurrentLinkedQueue = ConcurrentLinkedQueue() //could we use channel instead ?
- private val startedRequest: ConcurrentHashMap = ConcurrentHashMap()
+ private val startedRequests: ConcurrentHashMap = ConcurrentHashMap()
/**
* Add a SyncRequest into waiting queue if it matches some conditions:
@@ -68,17 +67,17 @@ object SyncProxy: SyncRequestCollector, SyncManager {
* @param request request to add to waiting queue
* @param context used to check previous failure of file sync
*/
- override fun queueSyncRequest(request: SyncRequest, context: Context) {
- for (syncWrapper in startedRequest.values) {
- if (syncWrapper.isRunning && syncWrapper == request) {
- return
- }
+ override fun queueSyncRequest(request: SyncRequest, context: Context): Boolean {
+ if (startedRequests.containsKey(request.syncedFileState.localPath)) {
+ Timber.d("A request is already performing for ${request.syncedFileState.name}")
+ return false
}
- if (skipBecauseOfPreviousFail(request, context)) return
+ if (skipBecauseOfPreviousFail(request, context)) return false
syncRequestQueue.remove(request)
syncRequestQueue.add(request)
+ return true
}
/**
@@ -91,11 +90,9 @@ object SyncProxy: SyncRequestCollector, SyncManager {
* @param context used to check previous failure of file sync
*/
override fun queueSyncRequests(requests: MutableCollection, context: Context) {
- for (syncWrapper in startedRequest.values) {
- if (syncWrapper.isRunning) {
- requests.removeIf { obj: SyncRequest? ->
- syncWrapper == obj
- }
+ for (syncWrapper in startedRequests.values) {
+ requests.removeIf { obj: SyncRequest ->
+ startedRequests.containsKey(obj.syncedFileState.localPath)
}
}
@@ -136,8 +133,10 @@ object SyncProxy: SyncRequestCollector, SyncManager {
}
if (previousSyncState != SyncState.SYNCHRONIZING) {
- val intent = Intent(context, SynchronizationService::class.java)
- context.startService(intent)
+ val workManager = WorkManager.getInstance(context)
+ val workRequest = WorkRequestFactory.getOneTimeWorkRequest(ONE_TIME_SYNC, null)
+
+ workManager.enqueueUniqueWork(SyncWorker.UNIQUE_WORK_NAME, ExistingWorkPolicy.KEEP, workRequest)
}
}
@@ -206,33 +205,25 @@ object SyncProxy: SyncRequestCollector, SyncManager {
return syncRequestQueue.poll()
}
- override fun addStartedRequest(threadId: Int, syncWrapper: SyncWrapper) {
- startedRequest[threadId] = syncWrapper
+ override fun addStartedRequest(fileLocalPath: String, syncWrapper: SyncWrapper) {
+ startedRequests[fileLocalPath] = syncWrapper
}
override fun isQueueEmpty(): Boolean {
return syncRequestQueue.isEmpty()
}
- override fun getStartedRequests(): ConcurrentHashMap {
- return startedRequest
- }
-
- override fun isAnySyncRequestRunning(): Boolean {
- return startedRequest.isNotEmpty()
+ override fun getQueueSize(): Int {
+ return syncRequestQueue.size
}
override fun clearRequestQueue() {
syncRequestQueue.clear()
}
- override fun getStartedRequestOnThread(threadIndex: Int): SyncWrapper? {
- return startedRequest[threadIndex]
- }
-
- override fun removeStartedRequest(threadIndex: Int) {
- if (startedRequest[threadIndex]?.isRunning == false) {
- startedRequest.remove(threadIndex)
+ override fun removeStartedRequest(fileLocalPath: String) {
+ if (startedRequests.containsKey(fileLocalPath)) {
+ startedRequests.remove(fileLocalPath)
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/foundation/e/drive/synchronization/SyncTask.kt b/app/src/main/java/foundation/e/drive/synchronization/SyncTask.kt
new file mode 100644
index 0000000000000000000000000000000000000000..d37317e846da5cc1d6433ff03a50f7a35a58ec43
--- /dev/null
+++ b/app/src/main/java/foundation/e/drive/synchronization/SyncTask.kt
@@ -0,0 +1,161 @@
+/*
+ * 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.accounts.Account
+import android.content.Context
+import com.owncloud.android.lib.common.OwnCloudClient
+import com.owncloud.android.lib.common.operations.RemoteOperationResult
+import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode.HOST_NOT_AVAILABLE
+import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode.NO_NETWORK_CONNECTION
+import com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode.WRONG_CONNECTION
+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.SyncWrapper
+import foundation.e.drive.synchronization.tasks.UploadFileOperation
+import foundation.e.drive.utils.CommonUtils
+import timber.log.Timber
+import java.io.File
+
+class SyncTask(
+ val request: SyncRequest,
+ val client: OwnCloudClient,
+ val account: Account,
+ val context: Context,
+) : Runnable {
+ companion object {
+ private val syncManager = SyncProxy as SyncManager
+ }
+
+ private val fileName = request.syncedFileState.name
+ private val fileLocalPath = request.syncedFileState.localPath
+
+ override fun run() {
+ if (!canStart()) {
+ return
+ }
+
+ Timber.d(" starts ${request.operationType.name} for: $fileLocalPath")
+
+ val wrapper = SyncWrapper(request, account, context)
+ syncManager.addStartedRequest(fileLocalPath, wrapper)
+
+ val succeed = when (request.operationType) {
+ UPLOAD -> runUpload(wrapper)
+ DOWNLOAD -> runDownload(wrapper)
+ DISABLE_SYNCING -> runSyncDisabling()
+ }
+
+ updateFailureCounter(request, succeed)
+ syncManager.removeStartedRequest(fileLocalPath)
+ Timber.d("${request.operationType.name} finished for $fileLocalPath")
+ }
+
+ private fun canStart(): Boolean {
+ val isSyncAllowed = CommonUtils.isThisSyncAllowed(account, request.syncedFileState.isMediaType)
+ if (!isSyncAllowed){
+ Timber.d("Sync of the file is not allowed anymore")
+ return false
+ }
+
+ if (!isNetworkAvailable()) {
+ Timber.d("No network available. can't run syncTask")
+ return false
+ }
+ return true
+ }
+
+
+ private fun runUpload(syncWrapper: SyncWrapper): Boolean {
+ @Suppress("DEPRECATION") val result = syncWrapper.remoteOperation?.execute(client)
+ if (result == null) {
+ Timber.d("Error: Upload result for $fileName is null")
+ return false
+ }
+
+ val code = result.code
+ Timber.d("Upload operation for $fileName result in: ${result.code}")
+
+ if (code == RemoteOperationResult.ResultCode.UNKNOWN_ERROR || code == RemoteOperationResult.ResultCode.FORBIDDEN) {
+ val operation = syncWrapper.remoteOperation as UploadFileOperation
+ val rowAffected = DbHelper.forceFoldertoBeRescan(operation.syncedState.id, context)
+ Timber.d("Force folder to be rescan next time (row affected) : $rowAffected")
+ return false
+ }
+
+ if (isNetworkLost(code)) {
+ syncManager.clearRequestQueue()
+ Timber.d("Network has been lost. SyncRequest queue has been cleared")
+ return false
+ }
+ return result.isSuccess
+ }
+
+ private fun runDownload(syncWrapper: SyncWrapper): Boolean {
+ @Suppress("DEPRECATION") val result = syncWrapper.remoteOperation?.execute(client)
+ if (result == null) {
+ Timber.d("Error: Download result for $fileName is null")
+ return false
+ }
+
+ val code = result.code
+ Timber.d("Download operation for $fileName result in: ${result.code}")
+
+ if (isNetworkLost(code)) {
+ syncManager.clearRequestQueue()
+ Timber.d("Network has been lost. SyncRequest queue has been cleared")
+ return false
+ }
+
+ return result.isSuccess
+ }
+
+ private fun runSyncDisabling(): Boolean {
+ val fileState = request.syncedFileState
+
+ fileState.disableScanning()
+ if (DbHelper.manageSyncedFileStateDB(fileState, "UPDATE", context) <= 0) {
+ Timber.d("Failed to disable sync for $fileState.name from DB")
+ return false
+ }
+
+ return true
+ }
+
+ private fun isNetworkLost(code: RemoteOperationResult.ResultCode): Boolean {
+ return (code == WRONG_CONNECTION
+ || code == NO_NETWORK_CONNECTION
+ || code == HOST_NOT_AVAILABLE)
+ }
+
+ private fun updateFailureCounter(request: SyncRequest, success: Boolean) {
+ val failedPrefs = FailedSyncPrefsManager.getInstance(context)
+ val fileState = request.syncedFileState
+ val fileStateId = fileState.id
+
+ if (!success) {
+ if (request.operationType == SyncRequest.Type.UPLOAD) {
+ val filePath = fileState.localPath
+ if (filePath.isEmpty()) return;
+ val file = File(filePath)
+ if (file.length() >= UploadFileOperation.FILE_SIZE_FLOOR_FOR_CHUNKED) return
+ }
+ failedPrefs.saveFileSyncFailure(fileStateId)
+ return
+ }
+
+ failedPrefs.removeDataForFile(fileStateId)
+ }
+
+ private fun isNetworkAvailable(): Boolean {
+ val isMeteredNetworkAllowed = CommonUtils.isMeteredNetworkAllowed(account)
+ return CommonUtils.haveNetworkConnection(context, isMeteredNetworkAllowed)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/foundation/e/drive/synchronization/SyncWorker.kt b/app/src/main/java/foundation/e/drive/synchronization/SyncWorker.kt
new file mode 100644
index 0000000000000000000000000000000000000000..eda410b26725e604426f23f46127c62c2224f48e
--- /dev/null
+++ b/app/src/main/java/foundation/e/drive/synchronization/SyncWorker.kt
@@ -0,0 +1,159 @@
+/*
+ * 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.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.utils.DavClientProvider
+import timber.log.Timber
+import java.util.concurrent.Executors
+import java.util.concurrent.Future
+import java.util.concurrent.TimeUnit
+
+class SyncWorker(
+ context: Context,
+ params: WorkerParameters
+) : Worker(context, params) {
+
+ companion object {
+ const val UNIQUE_WORK_NAME = "syncWorker"
+ const val NOTIF_CHANNEL_ID = "syncChannelId"
+ const val NOTIFICATION_ID = 2003004
+ 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)
+
+ override fun onStopped() {
+ Timber.d("SyncWorker has been stopped")
+ notificationManager.cancel(NOTIFICATION_ID)
+ executor.shutdownNow()
+
+ super.onStopped()
+ }
+
+ override fun doWork(): Result {
+ try {
+ account = loadAccount()
+ if (account == null) {
+ Timber.d("Warning : account is null")
+ syncManager.startListeningFiles(applicationContext as EdriveApplication)
+ return Result.failure()
+ }
+
+ ocClient = getOcClient(account!!)
+ if (ocClient == null) {
+ Timber.d("Warning : ocClient is null")
+ syncManager.startListeningFiles(applicationContext as EdriveApplication)
+ return Result.failure()
+ }
+
+ createNotificationChannel()
+
+ while (!syncManager.isQueueEmpty()) {
+ setForegroundAsync(createForegroundInfo())
+ executeRequests()
+ }
+
+ notificationManager.cancel(NOTIFICATION_ID)
+ syncManager.startListeningFiles(applicationContext as EdriveApplication)
+ } catch (exception: Exception) {
+ Timber.w(exception)
+ }
+
+ 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,
+ applicationContext
+ )
+ }
+
+ private fun executeRequests() {
+ val futureList = arrayListOf>()
+ if (executor.isShutdown || executor.isTerminated) {
+ executor = Executors.newFixedThreadPool(threadAmount)
+ }
+
+ while (!syncManager.isQueueEmpty()) {
+ val request = syncManager.pollSyncRequest()?: break
+ val task = SyncTask(request, ocClient!!, account!!, applicationContext)
+ futureList.add(executor!!.submit(task))
+ }
+
+ for (future: Future<*> in futureList) {
+ if (this.isStopped) {
+ Timber.d("Cancel task before execution")
+ future.cancel(true)
+ } else {
+ future.get()
+ }
+ }
+
+ executor.shutdown()
+ while (!executor.isTerminated && !executor.isShutdown) {
+ 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/operations/DownloadFileOperation.java b/app/src/main/java/foundation/e/drive/synchronization/tasks/DownloadFileOperation.java
similarity index 96%
rename from app/src/main/java/foundation/e/drive/operations/DownloadFileOperation.java
rename to app/src/main/java/foundation/e/drive/synchronization/tasks/DownloadFileOperation.java
index 942fd11d7117752f749a63b05bc1735de80bed5d..4874e4609c6c33b24d6f9f76e6a4cc0d14ac8bbd 100644
--- a/app/src/main/java/foundation/e/drive/operations/DownloadFileOperation.java
+++ b/app/src/main/java/foundation/e/drive/synchronization/tasks/DownloadFileOperation.java
@@ -7,10 +7,11 @@
* http://www.gnu.org/licenses/gpl.html
*/
-package foundation.e.drive.operations;
+package foundation.e.drive.synchronization.tasks;
import static com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode.FILE_NOT_FOUND;
import static com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode.FORBIDDEN;
+import static com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode.HOST_NOT_AVAILABLE;
import static com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode.INVALID_OVERWRITE;
import static com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode.NO_NETWORK_CONNECTION;
import static com.owncloud.android.lib.common.operations.RemoteOperationResult.ResultCode.OK;
@@ -129,7 +130,9 @@ public class DownloadFileOperation extends RemoteOperation {
private boolean isNetworkDisconnected(@NonNull final RemoteOperationResult result) {
RemoteOperationResult.ResultCode resultCode = result.getCode();
- return resultCode == NO_NETWORK_CONNECTION || resultCode == WRONG_CONNECTION;
+ return resultCode == NO_NETWORK_CONNECTION
+ || resultCode == WRONG_CONNECTION
+ || resultCode == HOST_NOT_AVAILABLE;
}
private RemoteOperationResult.ResultCode onDownloadSuccess(String tmpTargetFolderPath) {
diff --git a/app/src/main/java/foundation/e/drive/operations/UploadFileOperation.java b/app/src/main/java/foundation/e/drive/synchronization/tasks/UploadFileOperation.java
similarity index 97%
rename from app/src/main/java/foundation/e/drive/operations/UploadFileOperation.java
rename to app/src/main/java/foundation/e/drive/synchronization/tasks/UploadFileOperation.java
index 47cdd83e1870957d2b005cfa8132ae2d6f638132..e141198c227332893d809b1071251254de005a99 100644
--- a/app/src/main/java/foundation/e/drive/operations/UploadFileOperation.java
+++ b/app/src/main/java/foundation/e/drive/synchronization/tasks/UploadFileOperation.java
@@ -7,7 +7,7 @@
* http://www.gnu.org/licenses/gpl.html
*/
-package foundation.e.drive.operations;
+package foundation.e.drive.synchronization.tasks;
import static foundation.e.drive.utils.FileUtils.getMimeType;
@@ -131,6 +131,7 @@ public class UploadFileOperation extends RemoteOperation {
handledResultCodes.add(ResultCode.QUOTA_EXCEEDED);
handledResultCodes.add(ResultCode.WRONG_CONNECTION);
handledResultCodes.add(ResultCode.NO_NETWORK_CONNECTION);
+ handledResultCodes.add(ResultCode.HOST_NOT_AVAILABLE);
return handledResultCodes;
}
@@ -212,15 +213,17 @@ public class UploadFileOperation extends RemoteOperation {
@NonNull
public RemoteOperationResult checkAvailableSpace(@NonNull NextcloudClient client, long fileSize) {
final RemoteOperationResult ocsResult = readUserInfo(client);
- final ResultCode resultCode;
+ if (!ocsResult.isSuccess()) {
+ return ocsResult;
+ }
+
final Quota quotas = ocsResult.getResultData().getQuota();
- if (ocsResult.isSuccess() && quotas != null && quotas.getFree() < fileSize) {
- resultCode = ResultCode.QUOTA_EXCEEDED;
- } else {
- resultCode = ocsResult.getCode();
+ if (quotas != null && quotas.getFree() < fileSize) {
+ return new RemoteOperationResult(ResultCode.QUOTA_EXCEEDED);
}
- return new RemoteOperationResult(resultCode);
+
+ return ocsResult;
}
/**
@@ -267,6 +270,7 @@ public class UploadFileOperation extends RemoteOperation {
syncedState.getRemotePath(),
mimeType, syncedState.getLastEtag(),
timeStamp, false);
+
//noinspection deprecation
return uploadOperation.execute(client);
}
diff --git a/app/src/main/java/foundation/e/drive/work/FileSyncDisabler.java b/app/src/main/java/foundation/e/drive/work/FileSyncDisabler.java
deleted file mode 100644
index 76af0fd90d58106bcc8b281eacb9a2489986eae2..0000000000000000000000000000000000000000
--- a/app/src/main/java/foundation/e/drive/work/FileSyncDisabler.java
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright © MURENA 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.work;
-
-import android.content.Context;
-import android.os.Handler;
-
-import androidx.annotation.NonNull;
-
-import foundation.e.drive.database.DbHelper;
-import foundation.e.drive.models.SyncedFileState;
-import timber.log.Timber;
-
-/**
- * New Goal: Disable eDrive Synchronization of the given file
- *
- * previous goal: This worker is called when a remote file has been detected as removed and that all the
- * scanning process lead to choose to delete the local version too.
- *
- * @author vincent Bourgmayer
- */
-public class FileSyncDisabler {
-
- public interface FileSyncDisablingListener {
- void onSyncDisabled(int threadId, boolean succeed);
- }
-
- private final SyncedFileState fileState;
-
- public FileSyncDisabler(@NonNull SyncedFileState syncedFileState) {
- fileState = syncedFileState;
- }
-
- @NonNull
- public Runnable getRunnable(@NonNull final Handler handler, final int threadId, @NonNull final Context context, @NonNull final FileSyncDisablingListener listener) {
- return () -> {
-
- fileState.disableScanning();
- if (DbHelper.manageSyncedFileStateDB(fileState, "UPDATE", context) <= 0) {
- Timber.d("Failed to remove %s from DB", fileState.getName());
- }
-
- notifyCompletion(threadId, listener, true, handler);
- };
- }
-
- private void notifyCompletion(final int threadId, final @NonNull FileSyncDisablingListener listener, final boolean success, @NonNull final Handler handler) {
- handler.post(() -> listener.onSyncDisabled(threadId, success));
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/foundation/e/drive/work/WorkRequestFactory.java b/app/src/main/java/foundation/e/drive/work/WorkRequestFactory.java
index 324b5deffe149920cc984cd8b3a2f851be78f69e..460ed5761fb8714b00e86b32080691a92bba748b 100644
--- a/app/src/main/java/foundation/e/drive/work/WorkRequestFactory.java
+++ b/app/src/main/java/foundation/e/drive/work/WorkRequestFactory.java
@@ -40,6 +40,7 @@ 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 {
@@ -50,7 +51,8 @@ public class WorkRequestFactory {
ONE_TIME_APP_LIST,
ONE_TIME_USER_INFO,
ONE_TIME_ROOT_FOLDER_SETUP,
- ONE_TIME_FINISH_SETUP
+ ONE_TIME_FINISH_SETUP,
+ ONE_TIME_SYNC
}
private final static int PERIODIC_WORK_REPEAT_INTERVAL = 30;
@@ -135,11 +137,26 @@ public class WorkRequestFactory {
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
diff --git a/app/src/main/res/drawable/button_background.xml b/app/src/main/res/drawable/button_background.xml
index 1c1a64eee77dcdd4c2aa2f6d37208daae639dc62..56a100fd421de25d05e3202ab4de2a76a282a29b 100644
--- a/app/src/main/res/drawable/button_background.xml
+++ b/app/src/main/res/drawable/button_background.xml
@@ -1,4 +1,21 @@
+
+
+
diff --git a/app/src/main/res/drawable/button_background_light.xml b/app/src/main/res/drawable/button_background_light.xml
index 6a146ee343311301cbef4a7ef95b95d943f9c572..98394184d1a3c061d50599c2da358cd56f00df2a 100644
--- a/app/src/main/res/drawable/button_background_light.xml
+++ b/app/src/main/res/drawable/button_background_light.xml
@@ -1,4 +1,21 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_arrow_up.xml b/app/src/main/res/drawable/ic_arrow_up.xml
index fa9dfe41a36c6fc01654b409e7e52cd086847685..994851eb46e2cb934da6b73e26ff3ff494cd2f91 100644
--- a/app/src/main/res/drawable/ic_arrow_up.xml
+++ b/app/src/main/res/drawable/ic_arrow_up.xml
@@ -1,3 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/widget_background.xml b/app/src/main/res/drawable/widget_background.xml
index 33f8c8ad8fcb9fcf7d9179a459688791722ef31e..d79697ce59cb6cf3a3c016b3cd8d57ace19360ef 100644
--- a/app/src/main/res/drawable/widget_background.xml
+++ b/app/src/main/res/drawable/widget_background.xml
@@ -1,4 +1,21 @@
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 4e8c1cc12fe4e96dac163f0c30d821d8ea497ab3..fda41306fe16f9966de42198a8c4576956b5c720 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -31,7 +31,13 @@
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
+
+ - %d file to sync
+ - %d files to sync
+
+ Sync worker\'s channel
+ Notification about content being synchronized
Copied %1$s to clipboard
Settings
Additional settings for account
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 e57b66038dd59342bf6c82b0b58e0d9f8d7dcd0a..8bcb28ef1ebabab01c4339723c4e83d2576b33ee 100644
--- a/app/src/test/java/foundation/e/drive/operations/UploadFileOperationTest.java
+++ b/app/src/test/java/foundation/e/drive/operations/UploadFileOperationTest.java
@@ -41,6 +41,7 @@ import foundation.e.drive.database.DbHelper;
import foundation.e.drive.models.SyncedFileState;
import foundation.e.drive.models.SyncedFolder;
import foundation.e.drive.TestUtils;
+import foundation.e.drive.synchronization.tasks.UploadFileOperation;
import foundation.e.drive.utils.DavClientProvider;
@SuppressWarnings("rawtypes")