diff --git a/app/src/main/java/foundation/e/drive/EdriveApplication.java b/app/src/main/java/foundation/e/drive/EdriveApplication.java index 1e9b0d3289a92730dd351355aa67391676f1f818..fe9b303fa45e39dc128db77f2cf5da233227753b 100644 --- a/app/src/main/java/foundation/e/drive/EdriveApplication.java +++ b/app/src/main/java/foundation/e/drive/EdriveApplication.java @@ -12,14 +12,12 @@ import android.accounts.Account; import android.accounts.AccountManager; import android.app.Application; import android.content.Context; -import android.content.Intent; import android.content.SharedPreferences; import android.os.Environment; import foundation.e.drive.FileObservers.FileEventListener; import foundation.e.drive.FileObservers.RecursiveFileObserver; import foundation.e.drive.database.FailedSyncPrefsManager; -import foundation.e.drive.services.SynchronizationService; import foundation.e.drive.utils.AppConstants; import foundation.e.drive.utils.CommonUtils; import foundation.e.drive.utils.ReleaseTree; @@ -58,24 +56,20 @@ public class EdriveApplication extends Application { .apply(); } - startRecursiveFileObserver(); FailedSyncPrefsManager.getInstance(getApplicationContext()).clearPreferences(); - - final Intent SynchronizationServiceIntent = new Intent(getApplicationContext(), SynchronizationService.class); - startService(SynchronizationServiceIntent); } /** * Start Recursive FileObserver if not already watching */ - public void startRecursiveFileObserver() { + synchronized public void startRecursiveFileObserver() { if (!mFileObserver.isWatching()) { mFileObserver.startWatching(); Timber.d("Started RecursiveFileObserver on root folder"); } } - public void stopRecursiveFileObserver() { + synchronized public void stopRecursiveFileObserver() { mFileObserver.stopWatching(); Timber.d("Stopped RecursiveFileObserver on root folder"); } 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 e4e4dcbf56978d1e43e8608d8bd20cd0bbd0f4aa..aa621e6fc590bb97a8890f8c3807d37d848d2ce7 100644 --- a/app/src/main/java/foundation/e/drive/FileObservers/FileEventListener.java +++ b/app/src/main/java/foundation/e/drive/FileObservers/FileEventListener.java @@ -109,7 +109,7 @@ public class FileEventListener { final SyncRequestCollector syncManager = SyncProxy.INSTANCE; syncManager.queueSyncRequest(request, appContext.getApplicationContext()); - //todo service.startSynchronization(); + syncManager.startSynchronization(appContext); } 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 ce806ce1d6f92f8981b022ce16525592c8ec092e..1f67e5a741a0cd27d5c80d4dc578cef9b55d5569 100644 --- a/app/src/main/java/foundation/e/drive/receivers/BootCompletedReceiver.java +++ b/app/src/main/java/foundation/e/drive/receivers/BootCompletedReceiver.java @@ -8,6 +8,7 @@ package foundation.e.drive.receivers; +import android.app.Application; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -18,6 +19,7 @@ import androidx.annotation.NonNull; import foundation.e.drive.BuildConfig; import foundation.e.drive.database.DbHelper; +import foundation.e.drive.synchronization.SyncProxy; import foundation.e.drive.utils.AppConstants; import foundation.e.drive.utils.CommonUtils; import timber.log.Timber; @@ -51,6 +53,8 @@ public class BootCompletedReceiver extends BroadcastReceiver { Timber.e(exception); } } + + SyncProxy.INSTANCE.startListeningFiles((Application) context.getApplicationContext()); } } 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 147d9beeb0aba22229addec806a080e6acec076b..fe8faf1adaf7d6ab3106e0828f16b3d732d5eb0b 100644 --- a/app/src/main/java/foundation/e/drive/services/ObserverService.java +++ b/app/src/main/java/foundation/e/drive/services/ObserverService.java @@ -65,7 +65,6 @@ public class ObserverService extends Service implements OnRemoteOperationListene private final static int INTERSYNC_MINIMUM_DELAY = 900000; // min delay execution two sync in ms. private List mSyncedFolders; //List of synced folder - private boolean isWorking = false; private Account mAccount; private HashMap syncRequests; //integer is SyncedFileState id; Parcelable is the operation @@ -108,6 +107,7 @@ public class ObserverService extends Service implements OnRemoteOperationListene this.syncRequests = new HashMap<>(); if (!checkStartCondition(prefs, forcedSync)) { + syncManager.startListeningFiles(getApplication()); stopSelf(); return START_NOT_STICKY; } @@ -149,11 +149,6 @@ public class ObserverService extends Service implements OnRemoteOperationListene return false; } - if (isWorking) { - Timber.d("ObserverService is already working"); - return false; - } - // Check minimum delay since last call & not forced sync /*@todo is it really usefull to check time beetween to start as it is started by WorkManager? it matters only if we want to consider forced sync */ @@ -172,9 +167,9 @@ public class ObserverService extends Service implements OnRemoteOperationListene } - final boolean isSyncServicePaused = syncManager.pauseSync(); - Timber.d("isSyncServicePaused ? %s", isSyncServicePaused); - return isSyncServicePaused; + final boolean startAllowed = syncManager.onPeriodicScanStart(getApplication()); + Timber.d("starting periodic scan is allowed ? %s", startAllowed); + return startAllowed; } /* Common methods */ @@ -184,7 +179,6 @@ public class ObserverService extends Service implements OnRemoteOperationListene */ // protected to avoid SyntheticAccessor protected void begin(){ - this.isWorking = true; clearCachedFile(); deleteOldestCrashlogs(); startScan(true); @@ -321,20 +315,19 @@ public class ObserverService extends Service implements OnRemoteOperationListene startScan(false); - syncManager.unpauseSync(); - if (!syncRequests.isEmpty()) { Timber.d("syncRequests contains %s", syncRequests.size()); syncManager.queueSyncRequests(syncRequests.values(), getApplicationContext()); + syncManager.startSynchronization(getApplicationContext()); } else { Timber.i("There is no file to sync."); getSharedPreferences(AppConstants.SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE) .edit() .putLong(AppConstants.KEY_LAST_SYNC_TIME, System.currentTimeMillis()) .apply(); + syncManager.startListeningFiles(getApplication()); } - this.isWorking = false; this.stopSelf(); } 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 cc7b8512d6759ce310362a79e77f843c2e576c0b..42700dc3ebfed8f8d2b1527bb13173e1511afa43 100644 --- a/app/src/main/java/foundation/e/drive/services/SynchronizationService.java +++ b/app/src/main/java/foundation/e/drive/services/SynchronizationService.java @@ -78,6 +78,7 @@ public class SynchronizationService extends Service implements OnRemoteOperation if (account == null) { Timber.d("No account available"); + syncManager.startListeningFiles(getApplication()); stopSelf(); return START_NOT_STICKY; } @@ -93,6 +94,8 @@ public class SynchronizationService extends Service implements OnRemoteOperation handlerThread.start(); handler = new Handler(handlerThread.getLooper()); + startSynchronization(); + return START_REDELIVER_INTENT; } @@ -124,14 +127,18 @@ public class SynchronizationService extends Service implements OnRemoteOperation return; } - if (!canStart(threadIndex) || syncManager.isPaused()) { - Timber.d("Can't start thread #%s, Sync is paused or thread is already running", threadIndex); + 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; } @@ -147,7 +154,7 @@ public class SynchronizationService extends Service implements OnRemoteOperation final FileSyncDisabler fileSyncDisabler = new FileSyncDisabler(request.getSyncedFileState()); threadPool[threadIndex] = new Thread(fileSyncDisabler.getRunnable(handler, threadIndex, getApplicationContext(), this)); threadPool[threadIndex].start(); - syncManager.addStartedSync(threadIndex,syncWrapper); + syncManager.addStartedRequest(threadIndex,syncWrapper); return; } @@ -162,7 +169,7 @@ public class SynchronizationService extends Service implements OnRemoteOperation //noinspection deprecation threadPool[threadIndex] = operation.execute(ocClient, this, handler); - syncManager.addStartedSync(threadIndex, syncWrapper); + syncManager.addStartedRequest(threadIndex, syncWrapper); } /** @@ -171,7 +178,7 @@ public class SynchronizationService extends Service implements OnRemoteOperation * @return false if nogo */ private boolean canStart(int threadIndex) { - final SyncWrapper syncWrapper = syncManager.getStartedSync(threadIndex); + final SyncWrapper syncWrapper = syncManager.getStartedRequestOnThread(threadIndex); return (syncWrapper == null || !syncWrapper.isRunning()); } @@ -202,7 +209,11 @@ public class SynchronizationService extends Service implements OnRemoteOperation break; } - for (Map.Entry keyValue : syncManager.getStartedSync().entrySet()) { + 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)) { @@ -212,10 +223,6 @@ public class SynchronizationService extends Service implements OnRemoteOperation break; } } - - if (isNetworkDisconnected) { - syncManager.clearRequestQueue(); - } } private void logSyncResult(@NonNull RemoteOperationResult.ResultCode resultCode, @NonNull RemoteOperation callerOperation) { @@ -255,7 +262,7 @@ public class SynchronizationService extends Service implements OnRemoteOperation @Override public void onSyncDisabled(int threadId, boolean succeed) { - final SyncWrapper wrapper = syncManager.getStartedSync(threadId); + final SyncWrapper wrapper = syncManager.getStartedRequestOnThread(threadId); if (wrapper != null) { final SyncRequest request = wrapper.getRequest(); Timber.d("%s sync disabled ? %s", request.getSyncedFileState().getLocalPath(), succeed); diff --git a/app/src/main/java/foundation/e/drive/synchronization/StateMachine.kt b/app/src/main/java/foundation/e/drive/synchronization/StateMachine.kt new file mode 100644 index 0000000000000000000000000000000000000000..4407d6ce326d2cb792f1e8bc27e54aa117195873 --- /dev/null +++ b/app/src/main/java/foundation/e/drive/synchronization/StateMachine.kt @@ -0,0 +1,78 @@ +/* + * 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 androidx.annotation.VisibleForTesting +import timber.log.Timber + +enum class SyncState { + PERIODIC_SCAN, + SYNCHRONIZING, + LISTENING_FILES, + IDLE +} + +/** + * This class handle Synchronization state of the app + * 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) + * @author Vincent Bourgmayer + */ +object StateMachine { + + + var currentState: SyncState = SyncState.IDLE + @VisibleForTesting + set + + private var lastUpdateTimeInMs = 0L //todo use it to prevent apps to be blocked in one state + + @Synchronized + fun changeState(newState: SyncState): Boolean { + val previousState = currentState + val result = when (newState) { + SyncState.PERIODIC_SCAN -> setPeriodicScanState() + SyncState.SYNCHRONIZING -> setSynchronizing() + SyncState.LISTENING_FILES -> setListeningFilesState() + SyncState.IDLE -> false + } + if (result) { + Timber.i("Change Sync state to %s from %s", newState, previousState) + lastUpdateTimeInMs = System.currentTimeMillis() + } else { + Timber.d("Failed to change state to %s, from %s", newState, previousState) + } + return result + } + + private fun setListeningFilesState(): Boolean { + currentState = SyncState.LISTENING_FILES + return true + } + + private fun setPeriodicScanState(): Boolean { + if (currentState == SyncState.SYNCHRONIZING) { + Timber.d("Cannot change state: files sync is running") + return false + } + if (currentState == SyncState.PERIODIC_SCAN) { + Timber.d("Cannot change state: Periodic scan is already running") + return false + } + + currentState = SyncState.PERIODIC_SCAN + return true + } + + private fun setSynchronizing(): Boolean { + currentState = SyncState.SYNCHRONIZING + return true + } +} \ 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 14c4c49bf37513f8a4f4dc2edd341590c647eedb..463ada8259aeefec115788fb00691c3dbb07b822 100644 --- a/app/src/main/java/foundation/e/drive/synchronization/SyncProxy.kt +++ b/app/src/main/java/foundation/e/drive/synchronization/SyncProxy.kt @@ -7,10 +7,14 @@ */ package foundation.e.drive.synchronization +import android.app.Application import android.content.Context +import android.content.Intent +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 timber.log.Timber import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentLinkedQueue @@ -22,8 +26,9 @@ import java.util.concurrent.ConcurrentLinkedQueue interface SyncRequestCollector { fun queueSyncRequest(request: SyncRequest, context: Context) fun queueSyncRequests(requests: MutableCollection, context: Context) - fun pauseSync(): Boolean - fun unpauseSync() + fun startSynchronization(context: Context) + fun onPeriodicScanStart(application: Application): Boolean + fun startListeningFiles(application: Application) } /** @@ -32,18 +37,18 @@ interface SyncRequestCollector { */ interface SyncManager { fun pollSyncRequest(): SyncRequest? - fun addStartedSync(threadId: Int, syncWrapper: SyncWrapper) + fun addStartedRequest(threadId: Int, syncWrapper: SyncWrapper) fun isQueueEmpty(): Boolean - fun isPaused(): Boolean fun clearRequestQueue() - fun getStartedSync(threadIndex: Int): SyncWrapper? - fun getStartedSync(): ConcurrentHashMap + fun getStartedRequestOnThread(threadIndex: Int): SyncWrapper? + fun getStartedRequests(): ConcurrentHashMap + fun isAnySyncRequestRunning(): Boolean + fun removeStartedRequest(threadIndex: Int) + fun startListeningFiles(application: Application) } /** * This class goals is to act as a proxy between file's change detection & performing synchronization - * todo 1. should I rename to SyncProxy ? I feel that it is less clear what it is - * todo 2. Shouldn't I use an Object instead of a class ? * 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 @@ -52,9 +57,7 @@ interface SyncManager { */ object SyncProxy: SyncRequestCollector, SyncManager { private val syncRequestQueue: ConcurrentLinkedQueue = ConcurrentLinkedQueue() //could we use channel instead ? - private val startedSync: ConcurrentHashMap = ConcurrentHashMap() - private var pauseStartTime = 0L - private val maxPauseTimeInMs: Long = 300000 //5 minutes, might need to be adapted + private val startedRequest: ConcurrentHashMap = ConcurrentHashMap() /** * Add a SyncRequest into waiting queue if it matches some conditions: @@ -66,7 +69,7 @@ object SyncProxy: SyncRequestCollector, SyncManager { * @param context used to check previous failure of file sync */ override fun queueSyncRequest(request: SyncRequest, context: Context) { - for (syncWrapper in startedSync.values) { + for (syncWrapper in startedRequest.values) { if (syncWrapper.isRunning && syncWrapper == request) { return } @@ -88,7 +91,7 @@ object SyncProxy: SyncRequestCollector, SyncManager { * @param context used to check previous failure of file sync */ override fun queueSyncRequests(requests: MutableCollection, context: Context) { - for (syncWrapper in startedSync.values) { + for (syncWrapper in startedRequest.values) { if (syncWrapper.isRunning) { requests.removeIf { obj: SyncRequest? -> syncWrapper == obj @@ -103,22 +106,81 @@ object SyncProxy: SyncRequestCollector, SyncManager { syncRequestQueue.addAll(requests) } - override fun pauseSync(): Boolean { - pauseStartTime = System.currentTimeMillis() - val isQueueEmpty = syncRequestQueue.isEmpty() - val isNoStartedSync = - startedSync.values.stream().noneMatch { obj: SyncWrapper -> obj.isRunning } + /** + * Try to start synchronization + * + * some rules: + * - Restart FileEventListener if previous state what Periodic Scan + * - Start synchronization + * @param context use ApplicationContext + */ + override fun startSynchronization(context: Context) { + if (context !is EdriveApplication) { + Timber.d("Invalid parameter: startSynchronization(context)") + return + } + + if (syncRequestQueue.isEmpty()) { + Timber.d("Request queue is empty") + return + } + + val previousSyncState = StateMachine.currentState + val isStateChanged = StateMachine.changeState(SyncState.SYNCHRONIZING) + + if (!isStateChanged) return + + if (previousSyncState == SyncState.PERIODIC_SCAN) { + context.startRecursiveFileObserver() + } + + if (previousSyncState != SyncState.SYNCHRONIZING) { + val intent = Intent(context, SynchronizationService::class.java) + context.startService(intent) + } + } + + /** + * Called when periodic scan try to start + * @param application need EdriveApplication instance or will return false + * @return true if the periodic scan is allowed + */ + override fun onPeriodicScanStart(application: Application): Boolean { + if (application !is EdriveApplication) { + Timber.d("Invalid parameter: : startPeriodicScan(application)") + return false + } + + val isStateChanged = StateMachine.changeState(SyncState.PERIODIC_SCAN) + if (!isStateChanged) return false + + application.stopRecursiveFileObserver() - Timber.v("is queue empty ? %s ; is no started sync ? %s", isQueueEmpty, isNoStartedSync) - val canBePaused = isQueueEmpty && isNoStartedSync - if (!canBePaused) pauseStartTime = 0L - return canBePaused + return true } - override fun unpauseSync() { - pauseStartTime = 0L + /** + * File synchronization is finished + * restart FileObserver if required + * @param application need EdriveApplication instance or will exit + */ + override fun startListeningFiles(application: Application) { + if (application !is EdriveApplication) { + Timber.d("Invalid parameter: startListeningFiles(application)") + 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() + } } + /** * Progressively delay synchronization of a file in case of failure * @param request SyncRequest for the given file @@ -142,28 +204,33 @@ object SyncProxy: SyncRequestCollector, SyncManager { return syncRequestQueue.poll() } - override fun addStartedSync(threadId: Int, syncWrapper: SyncWrapper) { - startedSync[threadId] = syncWrapper + override fun addStartedRequest(threadId: Int, syncWrapper: SyncWrapper) { + startedRequest[threadId] = syncWrapper } override fun isQueueEmpty(): Boolean { return syncRequestQueue.isEmpty() } - override fun isPaused(): Boolean { - return if (pauseStartTime == 0L) false - else System.currentTimeMillis() - pauseStartTime < maxPauseTimeInMs + override fun getStartedRequests(): ConcurrentHashMap { + return startedRequest } - override fun getStartedSync(): ConcurrentHashMap { - return startedSync + override fun isAnySyncRequestRunning(): Boolean { + return startedRequest.isNotEmpty() } override fun clearRequestQueue() { syncRequestQueue.clear() } - override fun getStartedSync(threadIndex: Int): SyncWrapper? { - return startedSync[threadIndex] + override fun getStartedRequestOnThread(threadIndex: Int): SyncWrapper? { + return startedRequest[threadIndex] + } + + override fun removeStartedRequest(threadIndex: Int) { + if (startedRequest[threadIndex]?.isRunning == false) { + startedRequest.remove(threadIndex) + } } } \ No newline at end of file diff --git a/app/src/main/java/foundation/e/drive/work/FirstStartWorker.java b/app/src/main/java/foundation/e/drive/work/FirstStartWorker.java index aaf7f8e5ca345856b8fda4cb1c326c02a4d1afab..0ebce10f2fa3c48ccfd0f327ff9280b3c2cb94a9 100644 --- a/app/src/main/java/foundation/e/drive/work/FirstStartWorker.java +++ b/app/src/main/java/foundation/e/drive/work/FirstStartWorker.java @@ -55,11 +55,8 @@ public class FirstStartWorker extends Worker { enqueuePeriodicFileScanWorkRequest(appContext); - getApplicationContext().startService(new Intent(getApplicationContext(), foundation.e.drive.services.SynchronizationService.class)); getApplicationContext().startService(new Intent(getApplicationContext(), foundation.e.drive.services.ObserverService.class)); - ((EdriveApplication) getApplicationContext()).startRecursiveFileObserver(); - return Result.success(); } catch (Exception exception) { Timber.e(exception); diff --git a/app/src/test/java/foundation/e/drive/synchronization/StateMachineTest.kt b/app/src/test/java/foundation/e/drive/synchronization/StateMachineTest.kt new file mode 100644 index 0000000000000000000000000000000000000000..c1674fca0c2c14533afa3ad6430cb53bed1f1cd9 --- /dev/null +++ b/app/src/test/java/foundation/e/drive/synchronization/StateMachineTest.kt @@ -0,0 +1,183 @@ +/* + * 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 foundation.e.drive.synchronization.SyncState.IDLE +import foundation.e.drive.synchronization.SyncState.LISTENING_FILES +import foundation.e.drive.synchronization.SyncState.PERIODIC_SCAN +import foundation.e.drive.synchronization.SyncState.SYNCHRONIZING +import org.junit.Assert +import org.junit.Test + +class StateMachineTest { + + @Test + fun `Changing State from PERIODIC_SCAN to LISTENING_FILES should return true`() { + StateMachine.currentState = PERIODIC_SCAN + + val result = StateMachine.changeState(LISTENING_FILES) + Assert.assertTrue("Changing state from PERIODIC_SCAN to LISTENING_FILES returned false", result) + + val currentState = StateMachine.currentState + Assert.assertEquals("New current state should be LISTENING_FILES but is $currentState", LISTENING_FILES, currentState) + } + + @Test + fun `Changing State from PERIODIC_SCAN to PERIODIC_SCAN should return false`() { + StateMachine.currentState = PERIODIC_SCAN + + val result = StateMachine.changeState(PERIODIC_SCAN) + Assert.assertFalse("Changing state from PERIODIC_SCAN to PERIODIC_SCAN returned true", result) + + val currentState = StateMachine.currentState + Assert.assertEquals("Current state should remain PERIODIC_SCAN but is $currentState", PERIODIC_SCAN, currentState) + } + + @Test + fun `Changing State from PERIODIC_SCAN to SYNCHRONIZING should return true`() { + StateMachine.currentState = PERIODIC_SCAN + + val result = StateMachine.changeState(SYNCHRONIZING) + Assert.assertTrue("Changing state from PERIODIC_SCAN to SYNCHRONIZING returned false", result) + + val currentState = StateMachine.currentState + Assert.assertEquals("New current state should be SYNCHRONIZING but is $currentState", SYNCHRONIZING, currentState) + } + + @Test + fun `Changing State from PERIODIC_SCAN to IDLE should return false`() { + StateMachine.currentState = PERIODIC_SCAN + + val result = StateMachine.changeState(IDLE) + Assert.assertFalse("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) + } + + @Test + fun `Changing State from LISTENING_FILES to PERIODIC_SCAN should return true`() { + StateMachine.currentState = LISTENING_FILES + + val result = StateMachine.changeState(PERIODIC_SCAN) + Assert.assertTrue("Changing state from LISTENING_FILES to PERIODIC_SCAN returned false", result) + + val currentState = StateMachine.currentState + Assert.assertEquals("New current state should be PERIODIC_SCAN but is $currentState", PERIODIC_SCAN, currentState) + } + + @Test + fun `Changing State from LISTENING_FILE to SYNCHRONIZING should return true`() { + StateMachine.currentState = LISTENING_FILES + + val result = StateMachine.changeState(SYNCHRONIZING) + Assert.assertTrue("Changing state from LISTENING_FILES to SYNCHRONIZING returned false", result) + + val currentState = StateMachine.currentState + Assert.assertEquals("New current state should be SYNCHRONIZING but is $currentState", SYNCHRONIZING, currentState) + } + + @Test + fun `Changing State from LISTENING_FILE to IDLE should return false`() { + StateMachine.currentState = LISTENING_FILES + + val result = StateMachine.changeState(IDLE) + Assert.assertFalse("Changing state from LISTENING_FILES to IDLE returned true", result) + + val currentState = StateMachine.currentState + Assert.assertEquals("Current state should remain LISTENING_FILES but is $currentState", LISTENING_FILES, currentState) + } + + @Test + fun `Changing State from LISTENING_FILES to LISTENING_FILES should return true`() { + StateMachine.currentState = LISTENING_FILES + + val result = StateMachine.changeState(LISTENING_FILES) + Assert.assertTrue("Changing state from LISTENING_FILES to LISTENING_FILES returned false", result) + + val currentState = StateMachine.currentState + Assert.assertEquals("New current state should be LISTENING_FILES but is $currentState", LISTENING_FILES, currentState) + } + + @Test + fun `Changing State from SYNCHRONIZING to LISTENING_FILE should return true`() { + StateMachine.currentState = SYNCHRONIZING + + val result = StateMachine.changeState(LISTENING_FILES) + Assert.assertTrue("Changing state from SYNCHRONIZING to LISTENING_FILES returned false", result) + + val currentState = StateMachine.currentState + Assert.assertEquals("New current state should be LISTENING_FILES but is $currentState", LISTENING_FILES, currentState) + } + + @Test + fun `Changing State from SYNCHRONIZING to PERIODIC_SCAN should return false`() { + StateMachine.currentState = SYNCHRONIZING + + val result = StateMachine.changeState(PERIODIC_SCAN) + Assert.assertFalse("Changing state from SYNCHRONIZING to PERIODIC_SCAN returned true", result) + + val currentState = StateMachine.currentState + Assert.assertEquals("Current state should remain SYNCHRONIZING but is $currentState", SYNCHRONIZING, currentState) + } + + @Test + fun `Changing State from SYNCHRONIZING to SYNCHRONIZING should return true`() { + StateMachine.currentState = SYNCHRONIZING + + val result = StateMachine.changeState(SYNCHRONIZING) + Assert.assertTrue("Changing state from SYNCHRONIZING to SYNCHRONIZING returned false", result) + + val currentState = StateMachine.currentState + Assert.assertEquals("New Current state should be SYNCHRONIZING but is $currentState", SYNCHRONIZING, currentState) + } + + @Test + fun `Changing State from SYNCHRONIZING to IDLE should return false`() { + StateMachine.currentState = SYNCHRONIZING + + val result = StateMachine.changeState(IDLE) + Assert.assertFalse("Changing state from SYNCHRONIZING to IDLE returned true", result) + + val currentState = StateMachine.currentState + Assert.assertEquals("Current state should remain SYNCHRONIZING but is $currentState", SYNCHRONIZING, currentState) + } + + @Test + fun `Changing State from IDLE to LISTENING_FILES should return true`() { + StateMachine.currentState = IDLE + + val result = StateMachine.changeState(LISTENING_FILES) + Assert.assertTrue("Changing state from IDLE to LISTENING_FILES returned false", result) + + val currentState = StateMachine.currentState + Assert.assertEquals("New current state should be LISTENING_FILES but is $currentState", LISTENING_FILES, currentState) + } + + @Test + fun `Changing State from IDLE to SYNCHRONIZING should return true`() { + StateMachine.currentState = IDLE + + val result = StateMachine.changeState(SYNCHRONIZING) + Assert.assertTrue("Changing state from IDLE to SYNCHRONIZING returned false", result) + + val currentState = StateMachine.currentState + Assert.assertEquals("New current state should be SYNCHRONIZING but is $currentState", SYNCHRONIZING, currentState) + } + + @Test + fun `Changing State from IDLE to PERIODIC_SCAN should return true`() { + StateMachine.currentState = IDLE + + val result = StateMachine.changeState(PERIODIC_SCAN) + Assert.assertTrue("Changing state from IDLE to PERIODIC_SCAN returned false", result) + + val currentState = StateMachine.currentState + Assert.assertEquals("New current state should be PERIODIC_SCAN but is $currentState", PERIODIC_SCAN, currentState) + } +} \ No newline at end of file