Loading app/build.gradle +1 −1 Original line number Diff line number Diff line Loading @@ -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' } Loading app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java +9 −0 Original line number Diff line number Diff line Loading @@ -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); Loading app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java +59 −19 Original line number Diff line number Diff line 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; Loading @@ -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; Loading @@ -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 { Loading @@ -68,6 +67,9 @@ abstract class NotesServerSyncTask extends Thread { @NonNull protected final ArrayList<Throwable> 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; Loading @@ -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(); } Loading @@ -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); Loading @@ -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"); Loading @@ -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"); Loading @@ -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. Loading @@ -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()); } } } Loading @@ -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); Loading @@ -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); } /** Loading app/src/main/java/it/niedermann/owncloud/notes/shared/model/SyncResultStatus.java +3 −0 Original line number Diff line number Diff line Loading @@ -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; } } Loading
app/build.gradle +1 −1 Original line number Diff line number Diff line Loading @@ -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' } Loading
app/src/main/java/it/niedermann/owncloud/notes/persistence/ApiProvider.java +9 −0 Original line number Diff line number Diff line Loading @@ -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); Loading
app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java +59 −19 Original line number Diff line number Diff line 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; Loading @@ -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; Loading @@ -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 { Loading @@ -68,6 +67,9 @@ abstract class NotesServerSyncTask extends Thread { @NonNull protected final ArrayList<Throwable> 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; Loading @@ -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(); } Loading @@ -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); Loading @@ -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"); Loading @@ -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"); Loading @@ -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. Loading @@ -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()); } } } Loading @@ -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); Loading @@ -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); } /** Loading
app/src/main/java/it/niedermann/owncloud/notes/shared/model/SyncResultStatus.java +3 −0 Original line number Diff line number Diff line Loading @@ -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; } }