diff --git a/app/build.gradle b/app/build.gradle index 18799c3726eeb9e64f69a0b07e3d9f960d415944..fe05773c532e656e0be5d05b6ab614ccf4416604 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,8 +7,8 @@ def buildDate = { -> def appMajor = 3 def appMinor = 7 -def appPatch = 3 -def appVersionCode = 3007003 +def appPatch = 4 +def appVersionCode = 3007004 android { compileSdkVersion 33 diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java index b4f378e36a6291a9c59d1eebcc8967dd0531cb6f..dced641694d23c011f13d4124ca88c16ac836684 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java @@ -43,6 +43,8 @@ import com.nextcloud.android.sso.model.SingleSignOnAccount; import org.jetbrains.annotations.NotNull; +import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; @@ -87,9 +89,16 @@ public class NotesRepository { private static final String PREF_KEY_MIGRATION_DONE = "old_note_migration_done"; private static final String PREF_KEY_REMOTE_CONFLICT_RESOLVED = "remote_conflict_resolved_1"; + private static final String PREF_KEY_LAST_SYNC_ATTEMPT = "last_synchronisation_attempt"; + private static final String PREF_KEY_WAIT_BEFORE_NEXT_SYNC = "wait_before_next_synchronisation"; private static final String TAG = NotesRepository.class.getSimpleName(); + // Delay between first ServerDownException occurs, and next synchronisation attempt: 5seconds. + private static final long MIN_DELAY_AFTER_FAILED_SYNCHRONIZATION = 5000L; + // MAX backoff delay after a ServerDownException: 24 hours + private static final long MAX_DELAY_BETWEEN_SYNCHRONIZATIONS = 24 * 60 * 60 * 1000; + private static NotesRepository instance; private final ApiProvider apiProvider; @@ -239,7 +248,7 @@ public class NotesRepository { e.printStackTrace(); apiProvider.invalidateAPICache(); } - + serviceWasDown(false); db.getAccountDao().deleteAccount(account); notifyWidgets(); } @@ -777,12 +786,46 @@ public class NotesRepository { * This method respects the user preference "Sync on Wi-Fi only". *

* NoteServerSyncHelper observes changes in the network connection. + * + * It also handle exponential back of in case of server unavailability. Return false if the + * synchronisation should be delayed. * The current state can be retrieved with this method. * * @return true if sync is possible, otherwise false. */ public boolean isSyncPossible() { - return isSyncPossible; + if (!isSyncPossible) { + return false; + } + + final SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); + Instant lastSync = Instant.ofEpochMilli(sharedPreferences.getLong(PREF_KEY_LAST_SYNC_ATTEMPT, 0L)); + long waitBeforeNextAttemp = sharedPreferences.getLong(PREF_KEY_WAIT_BEFORE_NEXT_SYNC, 0L); + boolean cooldownTimeElapsed = lastSync.plus(waitBeforeNextAttemp, ChronoUnit.MILLIS).isBefore(Instant.now()); + if (!cooldownTimeElapsed) { + Log.i(TAG, "Sync NOT possible, previous ServiceWasDown error was less than " + waitBeforeNextAttemp + " ms ago."); + } + return cooldownTimeElapsed; + } + + public void serviceWasDown(boolean down) { + SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()); + long now = Instant.now().toEpochMilli(); + long waitBeforeNextAttempt = sharedPreferences.getLong(PREF_KEY_WAIT_BEFORE_NEXT_SYNC, 0L); + + if (down) { + waitBeforeNextAttempt = Math.min( + Math.max(waitBeforeNextAttempt * 2, MIN_DELAY_AFTER_FAILED_SYNCHRONIZATION), + MAX_DELAY_BETWEEN_SYNCHRONIZATIONS + ); + } else { + waitBeforeNextAttempt = 0L; + } + + sharedPreferences.edit() + .putLong(PREF_KEY_LAST_SYNC_ATTEMPT, now) + .putLong(PREF_KEY_WAIT_BEFORE_NEXT_SYNC, waitBeforeNextAttempt) + .apply(); } public boolean isNetworkConnected() { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java index 62c90f620d1e5fb2bae365129f7d6014b66f8807..550aafe9756d8553f885ca92419da68df4ca955e 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java @@ -31,6 +31,7 @@ import it.niedermann.owncloud.notes.BuildConfig; import it.niedermann.owncloud.notes.persistence.entity.Account; import it.niedermann.owncloud.notes.persistence.entity.Note; import it.niedermann.owncloud.notes.persistence.sync.NotesAPI; +import it.niedermann.owncloud.notes.persistence.sync.ServiceDownException; import it.niedermann.owncloud.notes.shared.model.DBStatus; import it.niedermann.owncloud.notes.shared.model.ISyncCallback; import it.niedermann.owncloud.notes.shared.model.SyncResultStatus; @@ -93,15 +94,24 @@ abstract class NotesServerSyncTask extends Thread { Log.i(TAG, "STARTING SYNCHRONIZATION"); final var status = new SyncResultStatus(); - pushLocalChanges(status); + try { + pushLocalChanges(status); - if (status.retrySync) { - retry(); - return; - } + if (status.retrySync) { + retry(); + return; + } + + if (!onlyLocalChanges) { + status.pullSuccessful = pullRemoteChanges(); + } - if (!onlyLocalChanges) { - status.pullSuccessful = pullRemoteChanges(); + repo.serviceWasDown(false); + } catch (ServiceDownException e) { + exceptions.add(e); + status.pushSuccessful = false; + status.pullSuccessful = false; + repo.serviceWasDown(true); } Log.i(TAG, "SYNCHRONIZATION FINISHED"); @@ -152,6 +162,8 @@ abstract class NotesServerSyncTask extends Thread { Log.e(TAG, " ...Tried to recreate \"" + note.getTitle() + "\" (#" + note.getId() + ") but the server response was null."); throw new Exception("Server returned null after recreating \"" + note.getTitle() + "\" (#" + note.getId() + ")"); } + } else if (editResponse.code() == HTTP_NOT_FOUND) { + throw new ServiceDownException("404 on POST a new note"); } else { handleException(createResponse.message()); } @@ -168,6 +180,8 @@ abstract class NotesServerSyncTask extends Thread { throw new Exception("Server returned null after creating \"" + note.getTitle() + "\" (#" + note.getId() + ")"); } repo.updateRemoteId(note.getId(), remoteNote.getRemoteId()); + } else if (createResponse.code() == HTTP_NOT_FOUND) { + throw new ServiceDownException("404 on POST a new note"); } else { handleException(createResponse.message()); } @@ -209,6 +223,8 @@ abstract class NotesServerSyncTask extends Thread { if (!retrySync) { exceptions.add(e); } + } catch (ServiceDownException e) { + throw e; } catch (Exception e) { if (e instanceof TokenMismatchException) { apiProvider.invalidateAPICache(ssoAccount); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ServiceDownException.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ServiceDownException.java new file mode 100644 index 0000000000000000000000000000000000000000..2845281516b624f0ddda3021fa116bbfb8e61b15 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ServiceDownException.java @@ -0,0 +1,7 @@ +package it.niedermann.owncloud.notes.persistence.sync; + +public class ServiceDownException extends RuntimeException { + public ServiceDownException(String message) { + super(message); + } +}