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 91463c3c49026be15a5f70e88ed23e3edc2812aa..57c342c86c4eb6c08a1538774b2632be49a11b78 100644 --- a/app/src/main/java/foundation/e/drive/FileObservers/FileEventListener.java +++ b/app/src/main/java/foundation/e/drive/FileObservers/FileEventListener.java @@ -103,7 +103,7 @@ public class FileEventListener { */ private void sendSyncRequestToSynchronizationService(SyncRequest request) { Timber.d("Sending a SyncRequest for %s", request.getSyncedFileState().getName()); - if (serviceConnection.isBoundToSynchronizationService()) { + if (serviceConnection.isBound()) { serviceConnection.getSynchronizationService().queueSyncRequest(request); serviceConnection.getSynchronizationService().startSynchronization(); }else{ @@ -233,14 +233,10 @@ public class FileEventListener { } public void unbindFromSynchronizationService(){ - if (serviceConnection.isBoundToSynchronizationService()) - appContext.unbindService(serviceConnection); - else - Timber.w("Not bound to SynchronizationService: can't unbind."); + serviceConnection.unBind(appContext); } public void bindToSynchronizationService(){ - final Intent SynchronizationServiceIntent = new Intent(appContext, SynchronizationService.class); - appContext.bindService(SynchronizationServiceIntent, serviceConnection, Context.BIND_AUTO_CREATE); + serviceConnection.bindService(appContext); } } diff --git a/app/src/main/java/foundation/e/drive/contentScanner/LocalContentScanner.java b/app/src/main/java/foundation/e/drive/contentScanner/LocalContentScanner.java index 0187ac57bfa6906e944995fe43ccd5beefc0d5da..65ad9d4e9c75fd6bc26c8288aa20810929bf3dd9 100644 --- a/app/src/main/java/foundation/e/drive/contentScanner/LocalContentScanner.java +++ b/app/src/main/java/foundation/e/drive/contentScanner/LocalContentScanner.java @@ -34,9 +34,7 @@ public class LocalContentScanner extends AbstractContentScanner{ @Override protected void onMissingFile(SyncedFileState fileState) { - if (!fileState.hasBeenSynchronizedOnce()) { - return; - } + if (!fileState.hasBeenSynchronizedOnce()) return; final File file = new File(fileState.getLocalPath()); @@ -89,6 +87,4 @@ public class LocalContentScanner extends AbstractContentScanner{ final String filePath = CommonUtils.getLocalPath(file); return fileState.getLocalPath().equals(filePath); } -} - - +} \ No newline at end of file diff --git a/app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java b/app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java index 342d9505fe61c56ebf77a19e60c44afafcaed721..9ff7c18c5c58bacadae5b383a9e5c02a487c4b74 100644 --- a/app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java +++ b/app/src/main/java/foundation/e/drive/contentScanner/RemoteContentScanner.java @@ -82,6 +82,11 @@ public class RemoteContentScanner extends AbstractContentScanner { @Override protected void onMissingFile(SyncedFileState fileState) { + if (!fileState.hasBeenSynchronizedOnce()) { + return; + } + + Timber.d("Add local deletion request for file: %s", fileState.getLocalPath()); this.syncRequests.put(fileState.getId(), new SyncRequest(fileState, LOCAL_DELETE)); } diff --git a/app/src/main/java/foundation/e/drive/models/SyncedFileState.java b/app/src/main/java/foundation/e/drive/models/SyncedFileState.java index 38ee333131ad353c274e266dcc83f1506a7ae5db..952fafbe3956a313aaf09711f2cbe9dc603046a9 100644 --- a/app/src/main/java/foundation/e/drive/models/SyncedFileState.java +++ b/app/src/main/java/foundation/e/drive/models/SyncedFileState.java @@ -15,8 +15,14 @@ import android.os.Parcelable; /** * @author Vincent Bourgmayer * Describe a file state which will be Synchronized (= Synced) or which has already been synced one times + * + * 1. file state: no etag & LM <=0 : new local file discovered + * todo: shouldn't it be more case 2 below ? would break lot of codes + * 2. filestate: no etag but LM > 0 : ??? + * When does it happens ?, and what does it correspond to ? what side effect of such case ? + * 3. fileState: etag but LM <= 0 : new remote file discovered + * 4. fileState: etag & LM > 0: file synchronized once */ - public class SyncedFileState implements Parcelable { public static final int NOT_SCANNABLE=0; public static final int ECLOUD_SCANNABLE=1; diff --git a/app/src/main/java/foundation/e/drive/operations/UploadFileOperation.java b/app/src/main/java/foundation/e/drive/operations/UploadFileOperation.java index 5e30d2e652c8dacc287c1ac9d29dc2f2131df66f..caa1874b3373270ff86de13b87958f88b8496f1b 100644 --- a/app/src/main/java/foundation/e/drive/operations/UploadFileOperation.java +++ b/app/src/main/java/foundation/e/drive/operations/UploadFileOperation.java @@ -133,7 +133,7 @@ public class UploadFileOperation extends RemoteOperation { //If file already up-to-date & synced if (syncedState.isLastEtagStored() && syncedState.getLocalLastModified() == file.lastModified()) { - Timber.d("Synchronization conflict because: last modified from DB(%s) and from file (%s) are different ", syncedState.getLocalLastModified(), file.lastModified()); + Timber.d("Synchronization conflict because: last modified from DB(%s) and from file (%s) are the same ", syncedState.getLocalLastModified(), file.lastModified()); return ResultCode.SYNC_CONFLICT; } diff --git a/app/src/main/java/foundation/e/drive/services/ObserverService.java b/app/src/main/java/foundation/e/drive/services/ObserverService.java index 3d201f7b2832b1856817d33898f30cb83f64b450..418b28df70030c7f8ac637063b7aac3783dfdde5 100644 --- a/app/src/main/java/foundation/e/drive/services/ObserverService.java +++ b/app/src/main/java/foundation/e/drive/services/ObserverService.java @@ -15,6 +15,7 @@ import static foundation.e.drive.utils.AppConstants.INITIALIZATION_HAS_BEEN_DONE import android.accounts.Account; import android.accounts.AccountManager; import android.app.Service; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -29,23 +30,19 @@ 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 com.owncloud.android.lib.resources.files.FileUtils; import com.owncloud.android.lib.resources.files.model.RemoteFile; import java.io.File; -import java.io.FileFilter; import java.io.FileOutputStream; import java.util.ArrayList; import java.util.HashMap; import java.util.List; -import java.util.ListIterator; import foundation.e.drive.contentScanner.LocalContentScanner; import foundation.e.drive.contentScanner.LocalFileLister; import foundation.e.drive.contentScanner.RemoteContentScanner; import foundation.e.drive.database.DbHelper; import foundation.e.drive.fileFilters.CrashlogsFileFilter; -import foundation.e.drive.fileFilters.FileFilterFactory; import foundation.e.drive.fileFilters.OnlyFileFilter; import foundation.e.drive.models.SyncRequest; import foundation.e.drive.models.SyncedFileState; @@ -72,32 +69,44 @@ public class ObserverService extends Service implements OnRemoteOperationListene private boolean isWorking = false; private Account mAccount; private HashMap syncRequests; //integer is SyncedFileState id; Parcelable is the operation - private SynchronizationServiceConnection synchronizationServiceConnection = new SynchronizationServiceConnection(); + private SynchronizationServiceConnection synchronizationServiceConnection = new SynchronizationServiceConnection() { + @Override + public void onServiceConnected(ComponentName componentName, IBinder iBinder) { + super.onServiceConnected(componentName, iBinder); + final SharedPreferences prefs = getApplicationContext().getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE); + + if (!checkStartCondition(prefs, forcedSync)) { + stopSelf(); + return; + } + + begin(); + } + }; private Handler handler; private HandlerThread handlerThread; + private boolean forcedSync = false; /* Lifecycle Methods */ @Override public void onDestroy(){ Timber.v("onDestroy()"); - unbindService(synchronizationServiceConnection); - handlerThread.quitSafely(); + synchronizationServiceConnection.unBind(getApplicationContext()); + if (handlerThread != null) handlerThread.quitSafely(); + mSyncedFolders = null; super.onDestroy(); - this.mSyncedFolders = null; } - @Override public void onCreate() { super.onCreate(); Timber.tag(ObserverService.class.getSimpleName()); + synchronizationServiceConnection.bindService(getApplicationContext()); } @Override public int onStartCommand(Intent intent, int flags, int startId) { Timber.i("onStartCommand(%s)", startId); - final Intent SynchronizationServiceIntent = new Intent(this, SynchronizationService.class); - bindService(SynchronizationServiceIntent, synchronizationServiceConnection, Context.BIND_AUTO_CREATE); CommonUtils.setServiceUnCaughtExceptionHandler(this); @@ -106,19 +115,11 @@ public class ObserverService extends Service implements OnRemoteOperationListene final String accountType = prefs.getString(AccountManager.KEY_ACCOUNT_TYPE, ""); this.mAccount = CommonUtils.getAccount(accountName, accountType, AccountManager.get(this)); - final boolean forcedSync = intent != null && DebugCmdReceiver.ACTION_FORCE_SYNC.equals(intent.getAction()); - - if (!checkStartCondition(prefs, forcedSync)) { - return super.onStartCommand(intent, flags, startId); - } + forcedSync = intent != null && DebugCmdReceiver.ACTION_FORCE_SYNC.equals(intent.getAction()); this.syncRequests = new HashMap<>(); - handlerThread = new HandlerThread("syncService_onResponse"); - handlerThread.start(); - handler = new Handler(handlerThread.getLooper()); - begin(); - return START_NOT_STICKY; + return START_STICKY; } /** @@ -170,14 +171,17 @@ public class ObserverService extends Service implements OnRemoteOperationListene return false; } - final boolean meteredNetworkAllowed = CommonUtils.isMeteredNetworkAllowed(mAccount); //check for the case where intent has been launched by initializerService if (!CommonUtils.haveNetworkConnection(this, meteredNetworkAllowed)) { Timber.w("There is no allowed internet connexion."); return false; } - return true; + + final boolean isSyncServicePaused = synchronizationServiceConnection.isBound() + && synchronizationServiceConnection.getSynchronizationService().pauseSync(); + Timber.d("isSyncServicePaused ? %s", isSyncServicePaused); + return isSyncServicePaused; } /* Common methods */ @@ -262,6 +266,9 @@ public class ObserverService extends Service implements OnRemoteOperationListene } try { + handlerThread = new HandlerThread("syncService_onResponse"); + handlerThread.start(); + handler = new Handler(handlerThread.getLooper()); final ListFileRemoteOperation loadOperation = new ListFileRemoteOperation(this.mSyncedFolders, this); loadOperation.execute(client, this, handler); } catch (IllegalArgumentException exception) { @@ -322,6 +329,10 @@ public class ObserverService extends Service implements OnRemoteOperationListene startScan(false); + if (synchronizationServiceConnection.isBound()) { + synchronizationServiceConnection.getSynchronizationService().unPauseSync(); + } + if (!syncRequests.isEmpty()) { Timber.d("syncRequests contains %s", syncRequests.size()); passSyncRequestsToSynchronizationService(); @@ -332,6 +343,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene .putLong(AppConstants.KEY_LAST_SYNC_TIME, System.currentTimeMillis()) .apply(); } + this.isWorking = false; this.stopSelf(); } @@ -340,11 +352,11 @@ public class ObserverService extends Service implements OnRemoteOperationListene * Send all gathered SyncRequest to SynchronizationService */ private void passSyncRequestsToSynchronizationService() { - if (synchronizationServiceConnection.isBoundToSynchronizationService()) { + if (synchronizationServiceConnection.isBound()) { synchronizationServiceConnection.getSynchronizationService().queueSyncRequests(syncRequests.values()); synchronizationServiceConnection.getSynchronizationService().startSynchronization(); } else { - Timber.e("ERROR: impossible to bind ObserverService to SynchronizationService"); + Timber.e("ERROR: binding to SynchronizationService lost"); } } diff --git a/app/src/main/java/foundation/e/drive/services/SynchronizationService.java b/app/src/main/java/foundation/e/drive/services/SynchronizationService.java index 77aa340ed51f270da4e665f90f2be1eb23b77a9c..0eaa40ffa49487a86f45b50551470589d8647b8d 100644 --- a/app/src/main/java/foundation/e/drive/services/SynchronizationService.java +++ b/app/src/main/java/foundation/e/drive/services/SynchronizationService.java @@ -51,6 +51,7 @@ import timber.log.Timber; */ public class SynchronizationService extends Service implements OnRemoteOperationListener, LocalFileDeleter.LocalFileDeletionListener { private final SynchronizationBinder binder = new SynchronizationBinder(); + private final static long maxPauseTimeInMs = 300000; //5 minutes, might need to be adapted private ConcurrentLinkedDeque syncRequestQueue; private ConcurrentHashMap startedSync; //Integer is thread index (1 to workerAmount) @@ -63,6 +64,8 @@ public class SynchronizationService extends Service implements OnRemoteOperation private NextcloudClient ncClient; private Handler handler; private HandlerThread handlerThread; + private long pauseStartTime = 0L; + @Override public void onCreate() { super.onCreate(); @@ -116,6 +119,35 @@ public class SynchronizationService extends Service implements OnRemoteOperation } + /** + * Try to pause synchronization. + * Can only be paused if no sync is already running and no request queue is started + * @return true if service successfully paused + */ + public boolean pauseSync() { + pauseStartTime = System.currentTimeMillis(); + + final boolean isQueueEmpty = syncRequestQueue.isEmpty(); + final boolean isNoStartedSync = startedSync.values().stream().noneMatch(predicate -> predicate.isRunning()); + + Timber.v("is queue empty ? %s ; is no started sync ? %s", isQueueEmpty, isNoStartedSync); + final boolean isPausable = isQueueEmpty && isNoStartedSync; + if (!isPausable) pauseStartTime = 0L; + return isPausable; + } + + /** + * unpause synchronization + */ + public void unPauseSync() { + pauseStartTime = 0L; + } + + private boolean isPaused() { + if (pauseStartTime == 0L) return false; + return System.currentTimeMillis() - pauseStartTime < maxPauseTimeInMs; + } + /** * Add a SyncRequest into waiting queue if it matches some conditions: * - an equivalent sync Request (same file & same operation type) isn't already running @@ -198,7 +230,7 @@ public class SynchronizationService extends Service implements OnRemoteOperation } private void startWorker(Integer threadIndex){ - if (!canStart(threadIndex)) return; + if (!canStart(threadIndex) || isPaused()) return; final SyncRequest request = this.syncRequestQueue.poll(); //return null if empty if (request == null @@ -210,6 +242,8 @@ public class SynchronizationService extends Service implements OnRemoteOperation final SyncWrapper syncWrapper = new SyncWrapper(request, account, getApplicationContext()); if (request.getOperationType().equals(SyncRequest.Type.LOCAL_DELETE)) { + Timber.v(" starts " + request.getSyncedFileState().getName() + + " local deletion on thread " + threadIndex); final LocalFileDeleter fileDeleter = new LocalFileDeleter(request.getSyncedFileState()); threadPool[threadIndex] = new Thread(fileDeleter.getRunnable( handler, threadIndex, getApplicationContext(), this)); threadPool[threadIndex].start(); diff --git a/app/src/main/java/foundation/e/drive/utils/SynchronizationServiceConnection.java b/app/src/main/java/foundation/e/drive/utils/SynchronizationServiceConnection.java index 5a11626cd1af7343aeab6cf4127fc0c443e9565c..370ed76e25f423e4ba7e2e772a38fc0677ea024a 100644 --- a/app/src/main/java/foundation/e/drive/utils/SynchronizationServiceConnection.java +++ b/app/src/main/java/foundation/e/drive/utils/SynchronizationServiceConnection.java @@ -9,9 +9,13 @@ package foundation.e.drive.utils; import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; +import androidx.annotation.NonNull; + import foundation.e.drive.services.SynchronizationService; import timber.log.Timber; @@ -21,26 +25,28 @@ import timber.log.Timber; public class SynchronizationServiceConnection implements ServiceConnection { private SynchronizationService synchronizationService; - private boolean boundToSynchronizationService = false; + private boolean isBound = false; + private boolean attemptingToBind = false; @Override public void onServiceConnected(ComponentName componentName, IBinder iBinder) { Timber.tag(SynchronizationServiceConnection.class.getSimpleName()) .v("binding to SynchronizationService"); - SynchronizationService.SynchronizationBinder binder = (SynchronizationService.SynchronizationBinder) iBinder; + attemptingToBind = false; + isBound = true; + final SynchronizationService.SynchronizationBinder binder = (SynchronizationService.SynchronizationBinder) iBinder; synchronizationService = binder.getService(); - boundToSynchronizationService = true; } @Override public void onServiceDisconnected(ComponentName componentName) { Timber.tag(SynchronizationServiceConnection.class.getSimpleName()) - .v("Unbinding from SynchronizationService"); - boundToSynchronizationService = false; + .v("onServiceDisconnected()"); + isBound = false; } - public boolean isBoundToSynchronizationService() { - return boundToSynchronizationService; + public boolean isBound() { + return isBound; } /** @@ -50,4 +56,27 @@ public class SynchronizationServiceConnection implements ServiceConnection { public SynchronizationService getSynchronizationService() { return synchronizationService; } + + /** + * Sources: https://stackoverflow.com/questions/8614335/android-how-to-safely-unbind-a-service + * @param context + */ + public void unBind(@NonNull Context context) { + attemptingToBind = false; + if (isBound) { + context.unbindService(this); + isBound = false; + } + } + + /** + * Sources: https://stackoverflow.com/questions/8614335/android-how-to-safely-unbind-a-service + * @param context + */ + public void bindService(@NonNull Context context) { + if (!attemptingToBind) { + attemptingToBind = true; + context.bindService(new Intent(context, SynchronizationService.class), this, Context.BIND_AUTO_CREATE); + } + } }