diff --git a/app/build.gradle b/app/build.gradle index 786286d7cb7534abee7c053566dee0b71bfd1695..e4b884d734a4d603f3e3a0840c1026b0a2a38260 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -56,7 +56,7 @@ dependencies { coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.6' // Nextcloud SSO - implementation 'foundation.e.lib:Android-SingleSignOn:1.0.4-alpha' + implementation 'foundation.e.lib:Android-SingleSignOn:1.0.5-alpha' implementation ('com.github.stefan-niedermann:android-commons:0.2.7') { exclude group: 'com.github.nextcloud', module: 'Android-SingleSignOn' } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java index 2caa3f8ff4a3d885adc6021570761d44ff5181f2..0cab114eeb1f2140e0612397dd9ce0c2e67130a8 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java @@ -77,6 +77,15 @@ public class ApiProvider { return notesAPI; } + public boolean isConnected(@NonNull SingleSignOnAccount ssoAccount) { + if (!API_CACHE.containsKey(ssoAccount.name)) { + return false; + } + + NextcloudAPI ncApi = API_CACHE.get(ssoAccount.name); + return ncApi.isConnected(); + } + private synchronized NextcloudAPI getNextcloudAPI(@NonNull Context context, @NonNull SingleSignOnAccount ssoAccount) { if (API_CACHE.containsKey(ssoAccount.name)) { return API_CACHE.get(ssoAccount.name); 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 c53f17e2c089fc20d9ef929bdb3baf86fef3e127..62c90f620d1e5fb2bae365129f7d6014b66f8807 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 @@ -1,12 +1,18 @@ package it.niedermann.owncloud.notes.persistence; +import static java.net.HttpURLConnection.HTTP_NOT_FOUND; +import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED; +import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; +import static it.niedermann.owncloud.notes.shared.model.DBStatus.LOCAL_DELETED; +import static it.niedermann.owncloud.notes.shared.util.NoteUtil.generateNoteExcerpt; + import android.content.Context; -import trikita.log.Log; +import android.os.DeadObjectException; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.nextcloud.android.sso.AccountImporter; -import com.nextcloud.android.sso.api.ParsedResponse; import com.nextcloud.android.sso.exceptions.NextcloudApiNotRespondingException; import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException; import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; @@ -20,7 +26,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import it.niedermann.owncloud.notes.BuildConfig; import it.niedermann.owncloud.notes.persistence.entity.Account; @@ -30,18 +35,12 @@ import it.niedermann.owncloud.notes.shared.model.DBStatus; import it.niedermann.owncloud.notes.shared.model.ISyncCallback; import it.niedermann.owncloud.notes.shared.model.SyncResultStatus; import it.niedermann.owncloud.notes.shared.util.ApiVersionUtil; -import retrofit2.Response; - -import static it.niedermann.owncloud.notes.shared.model.DBStatus.LOCAL_DELETED; -import static it.niedermann.owncloud.notes.shared.util.NoteUtil.generateNoteExcerpt; -import static java.net.HttpURLConnection.HTTP_NOT_FOUND; -import static java.net.HttpURLConnection.HTTP_NOT_MODIFIED; -import static java.net.HttpURLConnection.HTTP_UNAVAILABLE; +import trikita.log.Log; /** * {@link NotesServerSyncTask} is a {@link Thread} which performs the synchronization in a background thread. - * Synchronization consists of two parts: {@link #pushLocalChanges()} and {@link #pullRemoteChanges}. + * Synchronization consists of two parts: {@link #pushLocalChanges(SyncResultStatus)} and {@link #pullRemoteChanges}. */ abstract class NotesServerSyncTask extends Thread { @@ -68,6 +67,9 @@ abstract class NotesServerSyncTask extends Thread { @NonNull protected final ArrayList exceptions = new ArrayList<>(); + private static final int RETRY_LIMIT = 3; + private int retryCount = 0; + NotesServerSyncTask(@NonNull Context context, @NonNull NotesRepository repo, @NonNull Account localAccount, boolean onlyLocalChanges, @NonNull ApiProvider apiProvider) throws NextcloudFilesAppAccountNotFoundException { super(TAG); this.context = context; @@ -91,7 +93,13 @@ abstract class NotesServerSyncTask extends Thread { Log.i(TAG, "STARTING SYNCHRONIZATION"); final var status = new SyncResultStatus(); - status.pushSuccessful = pushLocalChanges(); + pushLocalChanges(status); + + if (status.retrySync) { + retry(); + return; + } + if (!onlyLocalChanges) { status.pullSuccessful = pullRemoteChanges(); } @@ -101,6 +109,11 @@ abstract class NotesServerSyncTask extends Thread { onPostExecute(status); } + private void retry() { + apiProvider.invalidateAPICache(ssoAccount); + run(); + } + abstract void onPreExecute(); abstract void onPostExecute(SyncResultStatus status); @@ -108,15 +121,16 @@ abstract class NotesServerSyncTask extends Thread { /** * Push local changes: for each locally created/edited/deleted Note, use NotesClient in order to push the changed to the server. */ - private boolean pushLocalChanges() { + private void pushLocalChanges(@NonNull SyncResultStatus status) { Log.d(TAG, "pushLocalChanges()"); boolean success = true; + boolean retrySync = false; final var notes = repo.getLocalModifiedNotes(localAccount.getId()); for (Note note : notes) { Log.d(TAG, " Process Local Note: " + (BuildConfig.DEBUG ? note : note.getTitle())); try { - Note remoteNote; + Note remoteNote = null; switch (note.getStatus()) { case LOCAL_EDITED: Log.v(TAG, " ...create/edit"); @@ -139,10 +153,10 @@ abstract class NotesServerSyncTask extends Thread { throw new Exception("Server returned null after recreating \"" + note.getTitle() + "\" (#" + note.getId() + ")"); } } else { - throw new Exception(createResponse.message()); + handleException(createResponse.message()); } } else { - throw new Exception(editResponse.message()); + handleException(editResponse.message()); } } else { Log.v(TAG, " ...Note does not have a remoteId yet → create"); @@ -155,7 +169,7 @@ abstract class NotesServerSyncTask extends Thread { } repo.updateRemoteId(note.getId(), remoteNote.getRemoteId()); } else { - throw new Exception(createResponse.message()); + handleException(createResponse.message()); } } // Please note, that db.updateNote() realized an optimistic conflict resolution, which is required for parallel changes of this Note from the UI. @@ -171,7 +185,7 @@ abstract class NotesServerSyncTask extends Thread { if (deleteResponse.code() == HTTP_NOT_FOUND) { Log.v(TAG, " ...delete (note has already been deleted remotely)"); } else { - throw new Exception(deleteResponse.message()); + handleException(deleteResponse.message()); } } } @@ -188,6 +202,13 @@ abstract class NotesServerSyncTask extends Thread { exceptions.add(e); success = false; } + } catch (DeadObjectException e) { + Log.e(TAG, e.getMessage(), e); + success = false; + retrySync = shouldRetry(); + if (!retrySync) { + exceptions.add(e); + } } catch (Exception e) { if (e instanceof TokenMismatchException) { apiProvider.invalidateAPICache(ssoAccount); @@ -196,7 +217,26 @@ abstract class NotesServerSyncTask extends Thread { success = false; } } - return success; + + status.pushSuccessful = success; + status.retrySync = retrySync; + } + + private boolean shouldRetry() { + retryCount++; + return (retryCount < RETRY_LIMIT) && !apiProvider.isConnected(ssoAccount); + } + + /** + * Sometimes AIDL service can be stopped on the middle of the operation. In that case, DeadObjectException can be thrown. + * We want to retry if that happened. This method pull out the specific exception to handle that. + */ + private void handleException(@Nullable String message) throws Exception { + if (message == null || !message.contains("DeadObjectException")) { + throw new Exception(message); + } + + throw new DeadObjectException(message); } /** diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/SyncResultStatus.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/SyncResultStatus.java index 41ba850abc6a0521f4b3dca86854a115ca4b542f..7c5f45dd21847553a647e1f7e127e827c35cf188 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/SyncResultStatus.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/SyncResultStatus.java @@ -4,10 +4,13 @@ public class SyncResultStatus { public boolean pullSuccessful = true; public boolean pushSuccessful = true; + public boolean retrySync = false; + public static final SyncResultStatus FAILED = new SyncResultStatus(); static { FAILED.pullSuccessful = false; FAILED.pushSuccessful = false; + FAILED.retrySync = false; } }