Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/stefan-niedermann/nextcloud-notes.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Niedermann <info@niedermann.it>2021-04-08 22:49:21 +0300
committerStefan Niedermann <info@niedermann.it>2021-04-08 22:49:21 +0300
commite9fc505ae098d33ca9dab61f478c60ca150a35b8 (patch)
tree992d1ccfa4a7b7a822e70319681e45c38e9c8d30 /app/src/main
parent4f8a9b4297e045ce0475f698dddcdd4e556e8341 (diff)
Get rid of deprecated AsyncTask
Diffstat (limited to 'app/src/main')
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java4
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteServerSyncHelper.java542
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java6
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncHelper.java346
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java232
-rw-r--r--app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java6
6 files changed, 586 insertions, 550 deletions
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java
index 4259d7f0..8c1b89f1 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainViewModel.java
@@ -32,7 +32,7 @@ import it.niedermann.owncloud.notes.branding.BrandingUtil;
import it.niedermann.owncloud.notes.main.navigation.NavigationAdapter;
import it.niedermann.owncloud.notes.main.navigation.NavigationItem;
import it.niedermann.owncloud.notes.persistence.CapabilitiesClient;
-import it.niedermann.owncloud.notes.persistence.NoteServerSyncHelper;
+import it.niedermann.owncloud.notes.persistence.NotesServerSyncHelper;
import it.niedermann.owncloud.notes.persistence.NotesDatabase;
import it.niedermann.owncloud.notes.persistence.entity.Account;
import it.niedermann.owncloud.notes.persistence.entity.CategoryWithNotesCount;
@@ -375,7 +375,7 @@ public class MainViewModel extends AndroidViewModel {
return new MutableLiveData<>(false);
} else {
Log.v(TAG, "[synchronize] - currentAccount: " + currentAccount.getAccountName());
- NoteServerSyncHelper syncHelper = db.getNoteServerSyncHelper();
+ NotesServerSyncHelper syncHelper = db.getNoteServerSyncHelper();
if (!syncHelper.isSyncPossible()) {
syncHelper.updateNetworkStatus();
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteServerSyncHelper.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteServerSyncHelper.java
deleted file mode 100644
index a6aebf7a..00000000
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NoteServerSyncHelper.java
+++ /dev/null
@@ -1,542 +0,0 @@
-package it.niedermann.owncloud.notes.persistence;
-
-import android.accounts.NetworkErrorException;
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.content.SharedPreferences;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.os.AsyncTask;
-import android.util.Log;
-
-import androidx.annotation.NonNull;
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MutableLiveData;
-import androidx.preference.PreferenceManager;
-
-import com.nextcloud.android.sso.AccountImporter;
-import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
-import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
-import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
-import com.nextcloud.android.sso.exceptions.TokenMismatchException;
-import com.nextcloud.android.sso.helper.SingleAccountHelper;
-import com.nextcloud.android.sso.model.SingleSignOnAccount;
-
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Set;
-
-import it.niedermann.owncloud.notes.R;
-import it.niedermann.owncloud.notes.persistence.entity.Account;
-import it.niedermann.owncloud.notes.persistence.entity.Note;
-import it.niedermann.owncloud.notes.shared.model.DBStatus;
-import it.niedermann.owncloud.notes.shared.model.ISyncCallback;
-import it.niedermann.owncloud.notes.shared.model.ServerResponse;
-import it.niedermann.owncloud.notes.shared.model.SyncResultStatus;
-import it.niedermann.owncloud.notes.shared.util.SSOUtil;
-
-import static androidx.lifecycle.Transformations.distinctUntilChanged;
-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;
-
-/**
- * Helps to synchronize the Database to the Server.
- */
-public class NoteServerSyncHelper {
-
- private static final String TAG = NoteServerSyncHelper.class.getSimpleName();
-
- private static NoteServerSyncHelper instance;
-
- private final NotesDatabase db;
- private final Context context;
-
- // Track network connection changes using a BroadcastReceiver
- private boolean isSyncPossible = false;
- private boolean networkConnected = false;
- private String syncOnlyOnWifiKey;
- private boolean syncOnlyOnWifi;
- private final MutableLiveData<Boolean> syncStatus = new MutableLiveData<>(false);
- private final MutableLiveData<ArrayList<Throwable>> syncErrors = new MutableLiveData<>();
-
- /**
- * @see <a href="https://stackoverflow.com/a/3104265">Do not make this a local variable.</a>
- */
- @SuppressWarnings("FieldCanBeLocal")
- private final SharedPreferences.OnSharedPreferenceChangeListener onSharedPreferenceChangeListener = (SharedPreferences prefs, String key) -> {
- if (syncOnlyOnWifiKey.equals(key)) {
- syncOnlyOnWifi = prefs.getBoolean(syncOnlyOnWifiKey, false);
- updateNetworkStatus();
- }
- };
-
- private final BroadcastReceiver networkReceiver = new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- updateNetworkStatus();
- if (isSyncPossible() && SSOUtil.isConfigured(context)) {
- new Thread(() -> {
- try {
- scheduleSync(db.getAccountDao().getAccountByName(SingleAccountHelper.getCurrentSingleSignOnAccount(context).name), false);
- } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
- Log.v(TAG, "Can not select current SingleSignOn account after network changed, do not sync.");
- }
- }).start();
- }
- }
- };
-
- // current state of the synchronization
- private final Map<Long, Boolean> syncActive = new HashMap<>();
- private final Map<Long, Boolean> syncScheduled = new HashMap<>();
-
- // list of callbacks for both parts of synchronization
- private final Map<Long, List<ISyncCallback>> callbacksPush = new HashMap<>();
- private final Map<Long, List<ISyncCallback>> callbacksPull = new HashMap<>();
-
- private NoteServerSyncHelper(NotesDatabase db) {
- this.db = db;
- this.context = db.getContext();
- this.syncOnlyOnWifiKey = context.getApplicationContext().getResources().getString(R.string.pref_key_wifi_only);
-
- // Registers BroadcastReceiver to track network connection changes.
- context.getApplicationContext().registerReceiver(networkReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
-
- SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.context.getApplicationContext());
- prefs.registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener);
- syncOnlyOnWifi = prefs.getBoolean(syncOnlyOnWifiKey, false);
-
- updateNetworkStatus();
- }
-
- /**
- * Get (or create) instance from NoteServerSyncHelper.
- * This has to be a singleton in order to realize correct registering and unregistering of
- * the BroadcastReceiver, which listens on changes of network connectivity.
- *
- * @param db {@link NotesDatabase}
- * @return NoteServerSyncHelper
- */
- public static synchronized NoteServerSyncHelper getInstance(NotesDatabase db) {
- if (instance == null) {
- instance = new NoteServerSyncHelper(db);
- }
- return instance;
- }
-
- @Override
- protected void finalize() throws Throwable {
- context.getApplicationContext().unregisterReceiver(networkReceiver);
- super.finalize();
- }
-
- /**
- * Synchronization is only possible, if there is an active network connection.
- * <p>
- * This method respects the user preference "Sync on Wi-Fi only".
- * <p>
- * NoteServerSyncHelper observes changes in the network connection.
- * The current state can be retrieved with this method.
- *
- * @return true if sync is possible, otherwise false.
- */
- public boolean isSyncPossible() {
- return isSyncPossible;
- }
-
- public boolean isNetworkConnected() {
- return networkConnected;
- }
-
- public boolean isSyncOnlyOnWifi() {
- return syncOnlyOnWifi;
- }
-
- /**
- * Adds a callback method to the NoteServerSyncHelper for the synchronization part push local changes to the server.
- * All callbacks will be executed once the synchronization operations are done.
- * After execution the callback will be deleted, so it has to be added again if it shall be
- * executed the next time all synchronize operations are finished.
- *
- * @param callback Implementation of ISyncCallback, contains one method that shall be executed.
- */
- public void addCallbackPush(Account account, ISyncCallback callback) {
- if (account == null) {
- Log.i(TAG, "ssoAccount is null. Is this a local account?");
- callback.onScheduled();
- callback.onFinish();
- } else {
- if (!callbacksPush.containsKey(account.getId())) {
- callbacksPush.put(account.getId(), new ArrayList<>());
- }
- Objects.requireNonNull(callbacksPush.get(account.getId())).add(callback);
- }
- }
-
- /**
- * Adds a callback method to the NoteServerSyncHelper for the synchronization part pull remote changes from the server.
- * All callbacks will be executed once the synchronization operations are done.
- * After execution the callback will be deleted, so it has to be added again if it shall be
- * executed the next time all synchronize operations are finished.
- *
- * @param callback Implementation of ISyncCallback, contains one method that shall be executed.
- */
- public void addCallbackPull(Account account, ISyncCallback callback) {
- if (account == null) {
- Log.i(TAG, "ssoAccount is null. Is this a local account?");
- callback.onScheduled();
- callback.onFinish();
- } else {
- if (!callbacksPull.containsKey(account.getId())) {
- callbacksPull.put(account.getId(), new ArrayList<>());
- }
- Objects.requireNonNull(callbacksPull.get(account.getId())).add(callback);
- }
- }
-
- /**
- * Schedules a synchronization and start it directly, if the network is connected and no
- * synchronization is currently running.
- *
- * @param onlyLocalChanges Whether to only push local changes to the server or to also load the whole list of notes from the server.
- */
- public void scheduleSync(Account account, boolean onlyLocalChanges) {
- if (account == null) {
- Log.i(TAG, SingleSignOnAccount.class.getSimpleName() + " is null. Is this a local account?");
- } else {
- if (syncActive.get(account.getId()) == null) {
- syncActive.put(account.getId(), false);
- }
- Log.d(TAG, "Sync requested (" + (onlyLocalChanges ? "onlyLocalChanges" : "full") + "; " + (Boolean.TRUE.equals(syncActive.get(account.getId())) ? "sync active" : "sync NOT active") + ") ...");
- if (isSyncPossible() && (!Boolean.TRUE.equals(syncActive.get(account.getId())) || onlyLocalChanges)) {
- try {
- SingleSignOnAccount ssoAccount = AccountImporter.getSingleSignOnAccount(context, account.getAccountName());
- Log.d(TAG, "... starting now");
- final NotesClient notesClient = NotesClient.newInstance(account.getPreferredApiVersion(), context);
- final SyncTask syncTask = new SyncTask(notesClient, account, ssoAccount, onlyLocalChanges);
- syncTask.addCallbacks(account, callbacksPush.get(account.getId()));
- callbacksPush.put(account.getId(), new ArrayList<>());
- if (!onlyLocalChanges) {
- syncTask.addCallbacks(account, callbacksPull.get(account.getId()));
- callbacksPull.put(account.getId(), new ArrayList<>());
- }
- syncTask.execute();
- } catch (NextcloudFilesAppAccountNotFoundException e) {
- Log.e(TAG, "... Could not find " + SingleSignOnAccount.class.getSimpleName() + " for account name " + account.getAccountName());
- e.printStackTrace();
- }
- } else if (!onlyLocalChanges) {
- Log.d(TAG, "... scheduled");
- syncScheduled.put(account.getId(), true);
- if (callbacksPush.containsKey(account.getId()) && callbacksPush.get(account.getId()) != null) {
- final List<ISyncCallback> callbacks = callbacksPush.get(account.getId());
- if (callbacks != null) {
- for (ISyncCallback callback : callbacks) {
- callback.onScheduled();
- }
- } else {
- Log.w(TAG, "List of push-callbacks was set for account \"" + account.getAccountName() + "\" but it was null");
- }
- }
- } else {
- Log.d(TAG, "... do nothing");
- if (callbacksPush.containsKey(account.getId()) && callbacksPush.get(account.getId()) != null) {
- final List<ISyncCallback> callbacks = callbacksPush.get(account.getId());
- if (callbacks != null) {
- for (ISyncCallback callback : callbacks) {
- callback.onScheduled();
- }
- } else {
- Log.w(TAG, "List of push-callbacks was set for account \"" + account.getAccountName() + "\" but it was null");
- }
- }
- }
- }
- }
-
- public void updateNetworkStatus() {
- try {
- final ConnectivityManager connMgr = (ConnectivityManager) context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
-
- if (connMgr == null) {
- throw new NetworkErrorException("ConnectivityManager is null");
- }
-
- final NetworkInfo activeInfo = connMgr.getActiveNetworkInfo();
-
- if (activeInfo == null) {
- throw new NetworkErrorException("NetworkInfo is null");
- }
-
- if (activeInfo.isConnected()) {
- networkConnected = true;
-
- final NetworkInfo networkInfo = connMgr.getNetworkInfo((ConnectivityManager.TYPE_WIFI));
-
- if (networkInfo == null) {
- throw new NetworkErrorException("connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI) is null");
- }
-
- isSyncPossible = !syncOnlyOnWifi || networkInfo.isConnected();
-
- if (isSyncPossible) {
- Log.d(TAG, "Network connection established.");
- } else {
- Log.d(TAG, "Network connected, but not used because only synced on wifi.");
- }
- } else {
- networkConnected = false;
- isSyncPossible = false;
- Log.d(TAG, "No network connection.");
- }
- } catch (NetworkErrorException e) {
- e.printStackTrace();
- networkConnected = false;
- isSyncPossible = false;
- }
- }
-
- /**
- * SyncTask is an AsyncTask which performs the synchronization in a background thread.
- * Synchronization consists of two parts: pushLocalChanges and pullRemoteChanges.
- */
- private class SyncTask extends AsyncTask<Void, Void, SyncResultStatus> {
- @NonNull
- private final NotesClient notesClient;
- @NonNull
- private final Account localAccount;
- @NonNull
- private final SingleSignOnAccount ssoAccount;
- private final boolean onlyLocalChanges;
- @NonNull
- private final Map<Long, List<ISyncCallback>> callbacks = new HashMap<>();
- @NonNull
- private final ArrayList<Throwable> exceptions = new ArrayList<>();
-
- SyncTask(@NonNull NotesClient notesClient, @NonNull Account localAccount, @NonNull SingleSignOnAccount ssoAccount, boolean onlyLocalChanges) {
- this.notesClient = notesClient;
- this.localAccount = localAccount;
- this.ssoAccount = ssoAccount;
- this.onlyLocalChanges = onlyLocalChanges;
- }
-
- private void addCallbacks(Account account, List<ISyncCallback> callbacks) {
- this.callbacks.put(account.getId(), callbacks);
- }
-
- @Override
- protected void onPreExecute() {
- super.onPreExecute();
- syncStatus.postValue(true);
- if (!syncScheduled.containsKey(localAccount.getId()) || syncScheduled.get(localAccount.getId()) == null) {
- syncScheduled.put(localAccount.getId(), false);
- }
- if (!onlyLocalChanges && Boolean.TRUE.equals(syncScheduled.get(localAccount.getId()))) {
- syncScheduled.put(localAccount.getId(), false);
- }
- syncActive.put(localAccount.getId(), true);
- }
-
- @Override
- protected SyncResultStatus doInBackground(Void... voids) {
- Log.i(TAG, "STARTING SYNCHRONIZATION");
- final SyncResultStatus status = new SyncResultStatus();
- status.pushSuccessful = pushLocalChanges();
- if (!onlyLocalChanges) {
- status.pullSuccessful = pullRemoteChanges();
- }
- Log.i(TAG, "SYNCHRONIZATION FINISHED");
- return status;
- }
-
- /**
- * Push local changes: for each locally created/edited/deleted Note, use NotesClient in order to push the changed to the server.
- */
- private boolean pushLocalChanges() {
- Log.d(TAG, "pushLocalChanges()");
-
- boolean success = true;
- List<Note> notes = db.getNoteDao().getLocalModifiedNotes(localAccount.getId());
- for (Note note : notes) {
- Log.d(TAG, " Process Local Note: " + note);
- try {
- Note remoteNote;
- switch (note.getStatus()) {
- case LOCAL_EDITED:
- Log.v(TAG, " ...create/edit");
- if (note.getRemoteId() != null) {
- Log.v(TAG, " ...Note has remoteId → try to edit");
- try {
- remoteNote = notesClient.editNote(ssoAccount, note).getNote();
- } catch (NextcloudHttpRequestFailedException e) {
- if (e.getStatusCode() == HTTP_NOT_FOUND) {
- Log.v(TAG, " ...Note does no longer exist on server → recreate");
- remoteNote = notesClient.createNote(ssoAccount, note).getNote();
- } else {
- throw e;
- }
- }
- } else {
- Log.v(TAG, " ...Note does not have a remoteId yet → create");
- remoteNote = notesClient.createNote(ssoAccount, note).getNote();
- db.getNoteDao().updateRemoteId(note.getId(), remoteNote.getRemoteId());
- }
- // Please note, that db.updateNote() realized an optimistic conflict resolution, which is required for parallel changes of this Note from the UI.
- // TODO: check if the Rooms implementation does this correctly!
- db.getNoteDao().updateIfNotModifiedLocallyDuringSync(note.getId(), remoteNote.getModified().getTimeInMillis(), remoteNote.getTitle(), remoteNote.getFavorite(), remoteNote.getETag(), remoteNote.getContent(), generateNoteExcerpt(remoteNote.getContent(), remoteNote.getTitle()), note.getContent(), note.getCategory(), note.getFavorite());
- break;
- case LOCAL_DELETED:
- if (note.getRemoteId() == null) {
- Log.v(TAG, " ...delete (only local, since it has never been synchronized)");
- } else {
- Log.v(TAG, " ...delete (from server and local)");
- try {
- notesClient.deleteNote(ssoAccount, note.getRemoteId());
- } catch (NextcloudHttpRequestFailedException e) {
- if (e.getStatusCode() == HTTP_NOT_FOUND) {
- Log.v(TAG, " ...delete (note has already been deleted remotely)");
- } else {
- throw e;
- }
- }
- }
- // Please note, that db.deleteNote() realizes an optimistic conflict resolution, which is required for parallel changes of this Note from the UI.
- db.getNoteDao().deleteByNoteId(note.getId(), LOCAL_DELETED);
- break;
- default:
- throw new IllegalStateException("Unknown State of Note " + note + ": " + note.getStatus());
- }
- } catch (NextcloudHttpRequestFailedException e) {
- if (e.getStatusCode() == HTTP_NOT_MODIFIED) {
- Log.d(TAG, "Server returned HTTP Status Code 304 - Not Modified");
- } else {
- exceptions.add(e);
- success = false;
- }
- } catch (Exception e) {
- if (e instanceof TokenMismatchException) {
- SSOClient.invalidateAPICache(ssoAccount);
- }
- exceptions.add(e);
- success = false;
- }
- }
- return success;
- }
-
- /**
- * Pull remote Changes: update or create each remote note (if local pendant has no changes) and remove remotely deleted notes.
- */
- private boolean pullRemoteChanges() {
- Log.d(TAG, "pullRemoteChanges() for account " + localAccount.getAccountName());
- try {
- final Map<Long, Long> idMap = db.getIdMap(localAccount.getId());
- final Calendar modified = localAccount.getModified();
- final long modifiedForServer = modified == null ? 0 : modified.getTimeInMillis() / 1_000;
- final ServerResponse.NotesResponse response = notesClient.getNotes(ssoAccount, modifiedForServer, localAccount.getETag());
- List<Note> remoteNotes = response.getNotes();
- Set<Long> remoteIDs = new HashSet<>();
- // pull remote changes: update or create each remote note
- for (Note remoteNote : remoteNotes) {
- Log.v(TAG, " Process Remote Note: " + remoteNote);
- remoteIDs.add(remoteNote.getRemoteId());
- if (remoteNote.getModified() == null) {
- Log.v(TAG, " ... unchanged");
- } else if (idMap.containsKey(remoteNote.getRemoteId())) {
- Log.v(TAG, " ... found → Update");
- Long localId = idMap.get(remoteNote.getRemoteId());
- if (localId != null) {
- db.getNoteDao().updateIfNotModifiedLocallyAndAnyRemoteColumnHasChanged(
- localId, remoteNote.getModified().getTimeInMillis(), remoteNote.getTitle(), remoteNote.getFavorite(), remoteNote.getCategory(), remoteNote.getETag(), remoteNote.getContent(), generateNoteExcerpt(remoteNote.getContent(), remoteNote.getTitle()));
- } else {
- Log.e(TAG, "Tried to update note from server, but local id of note is null. " + remoteNote);
- }
- } else {
- Log.v(TAG, " ... create");
- db.addNote(localAccount.getId(), remoteNote);
- }
- }
- Log.d(TAG, " Remove remotely deleted Notes (only those without local changes)");
- // remove remotely deleted notes (only those without local changes)
- for (Map.Entry<Long, Long> entry : idMap.entrySet()) {
- if (!remoteIDs.contains(entry.getKey())) {
- Log.v(TAG, " ... remove " + entry.getValue());
- db.getNoteDao().deleteByNoteId(entry.getValue(), DBStatus.VOID);
- }
- }
-
- // update ETag and Last-Modified in order to reduce size of next response
- localAccount.setETag(response.getETag());
- Calendar calendar = Calendar.getInstance();
- calendar.setTimeInMillis(response.getLastModified());
- localAccount.setModified(calendar);
- db.getAccountDao().updateETag(localAccount.getId(), localAccount.getETag());
- db.getAccountDao().updateModified(localAccount.getId(), localAccount.getModified().getTimeInMillis());
- try {
- if (db.updateApiVersion(localAccount.getId(), response.getSupportedApiVersions())) {
- localAccount.setApiVersion(response.getSupportedApiVersions());
- }
- } catch (Exception e) {
- exceptions.add(e);
- }
- return true;
- } catch (NextcloudHttpRequestFailedException e) {
- Log.d(TAG, "Server returned HTTP Status Code " + e.getStatusCode() + " - " + e.getMessage());
- if (e.getStatusCode() == HTTP_NOT_MODIFIED) {
- return true;
- } else {
- exceptions.add(e);
- return false;
- }
- } catch (Exception e) {
- if (e instanceof TokenMismatchException) {
- SSOClient.invalidateAPICache(ssoAccount);
- }
- exceptions.add(e);
- return false;
- }
- }
-
- @Override
- protected void onPostExecute(SyncResultStatus status) {
- super.onPostExecute(status);
- for (Throwable e : exceptions) {
- Log.e(TAG, e.getMessage(), e);
- }
- if (!status.pullSuccessful || !status.pushSuccessful) {
- syncErrors.postValue(exceptions);
- }
- syncActive.put(localAccount.getId(), false);
- // notify callbacks
- if (callbacks.containsKey(localAccount.getId()) && callbacks.get(localAccount.getId()) != null) {
- for (ISyncCallback callback : Objects.requireNonNull(callbacks.get(localAccount.getId()))) {
- callback.onFinish();
- }
- }
- db.notifyWidgets();
- db.updateDynamicShortcuts(localAccount.getId());
- // start next sync if scheduled meanwhile
- if (syncScheduled.containsKey(localAccount.getId()) && syncScheduled.get(localAccount.getId()) != null && Boolean.TRUE.equals(syncScheduled.get(localAccount.getId()))) {
- scheduleSync(localAccount, false);
- }
- syncStatus.postValue(false);
- }
- }
-
- public LiveData<Boolean> getSyncStatus() {
- return distinctUntilChanged(this.syncStatus);
- }
-
- public LiveData<ArrayList<Throwable>> getSyncErrors() {
- return this.syncErrors;
- }
-}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java
index 334728c5..87ee7b20 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java
@@ -96,7 +96,7 @@ public abstract class NotesDatabase extends RoomDatabase {
private static final String NOTES_DB_NAME = "OWNCLOUD_NOTES";
private static NotesDatabase instance;
private static Context context;
- private static NoteServerSyncHelper serverSyncHelper;
+ private static NotesServerSyncHelper serverSyncHelper;
private static String defaultNonEmptyTitle;
private static NotesDatabase create(final Context context) {
@@ -149,12 +149,12 @@ public abstract class NotesDatabase extends RoomDatabase {
if (instance == null) {
instance = create(context.getApplicationContext());
NotesDatabase.context = context.getApplicationContext();
- NotesDatabase.serverSyncHelper = NoteServerSyncHelper.getInstance(instance);
+ NotesDatabase.serverSyncHelper = NotesServerSyncHelper.getInstance(instance);
}
return instance;
}
- public NoteServerSyncHelper getNoteServerSyncHelper() {
+ public NotesServerSyncHelper getNoteServerSyncHelper() {
return NotesDatabase.serverSyncHelper;
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncHelper.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncHelper.java
new file mode 100644
index 00000000..e9fab1b0
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncHelper.java
@@ -0,0 +1,346 @@
+package it.niedermann.owncloud.notes.persistence;
+
+import android.accounts.NetworkErrorException;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.preference.PreferenceManager;
+
+import com.nextcloud.android.sso.AccountImporter;
+import com.nextcloud.android.sso.exceptions.NextcloudFilesAppAccountNotFoundException;
+import com.nextcloud.android.sso.exceptions.NoCurrentAccountSelectedException;
+import com.nextcloud.android.sso.helper.SingleAccountHelper;
+import com.nextcloud.android.sso.model.SingleSignOnAccount;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import it.niedermann.owncloud.notes.R;
+import it.niedermann.owncloud.notes.persistence.entity.Account;
+import it.niedermann.owncloud.notes.shared.model.ISyncCallback;
+import it.niedermann.owncloud.notes.shared.model.SyncResultStatus;
+import it.niedermann.owncloud.notes.shared.util.SSOUtil;
+
+import static androidx.lifecycle.Transformations.distinctUntilChanged;
+
+/**
+ * Helps to synchronize the Database to the Server.
+ */
+public class NotesServerSyncHelper {
+
+ private static final String TAG = NotesServerSyncHelper.class.getSimpleName();
+
+ private final ExecutorService executor = Executors.newSingleThreadExecutor();
+
+ private static NotesServerSyncHelper instance;
+
+ private final NotesDatabase db;
+ private final Context context;
+
+ /**
+ * Track network connection changes using a {@link BroadcastReceiver}
+ */
+ private boolean isSyncPossible = false;
+ private boolean networkConnected = false;
+ private String syncOnlyOnWifiKey;
+ private boolean syncOnlyOnWifi;
+ private final MutableLiveData<Boolean> syncStatus = new MutableLiveData<>(false);
+ private final MutableLiveData<ArrayList<Throwable>> syncErrors = new MutableLiveData<>();
+
+ /**
+ * @see <a href="https://stackoverflow.com/a/3104265">Do not make this a local variable.</a>
+ */
+ @SuppressWarnings("FieldCanBeLocal")
+ private final SharedPreferences.OnSharedPreferenceChangeListener onSharedPreferenceChangeListener = (SharedPreferences prefs, String key) -> {
+ if (syncOnlyOnWifiKey.equals(key)) {
+ syncOnlyOnWifi = prefs.getBoolean(syncOnlyOnWifiKey, false);
+ updateNetworkStatus();
+ }
+ };
+
+ private final BroadcastReceiver networkReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ updateNetworkStatus();
+ if (isSyncPossible() && SSOUtil.isConfigured(context)) {
+ new Thread(() -> {
+ try {
+ scheduleSync(db.getAccountDao().getAccountByName(SingleAccountHelper.getCurrentSingleSignOnAccount(context).name), false);
+ } catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
+ Log.v(TAG, "Can not select current SingleSignOn account after network changed, do not sync.");
+ }
+ }).start();
+ }
+ }
+ };
+
+ // current state of the synchronization
+ private final Map<Long, Boolean> syncActive = new HashMap<>();
+ private final Map<Long, Boolean> syncScheduled = new HashMap<>();
+
+ // list of callbacks for both parts of synchronization
+ private final Map<Long, List<ISyncCallback>> callbacksPush = new HashMap<>();
+ private final Map<Long, List<ISyncCallback>> callbacksPull = new HashMap<>();
+
+ private NotesServerSyncHelper(NotesDatabase db) {
+ this.db = db;
+ this.context = db.getContext();
+ this.syncOnlyOnWifiKey = context.getApplicationContext().getResources().getString(R.string.pref_key_wifi_only);
+
+ // Registers BroadcastReceiver to track network connection changes.
+ context.getApplicationContext().registerReceiver(networkReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION));
+
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this.context.getApplicationContext());
+ prefs.registerOnSharedPreferenceChangeListener(onSharedPreferenceChangeListener);
+ syncOnlyOnWifi = prefs.getBoolean(syncOnlyOnWifiKey, false);
+
+ updateNetworkStatus();
+ }
+
+ /**
+ * Get (or create) instance from NoteServerSyncHelper.
+ * This has to be a singleton in order to realize correct registering and unregistering of
+ * the BroadcastReceiver, which listens on changes of network connectivity.
+ *
+ * @param db {@link NotesDatabase}
+ * @return NoteServerSyncHelper
+ */
+ public static synchronized NotesServerSyncHelper getInstance(NotesDatabase db) {
+ if (instance == null) {
+ instance = new NotesServerSyncHelper(db);
+ }
+ return instance;
+ }
+
+ @Override
+ protected void finalize() throws Throwable {
+ context.getApplicationContext().unregisterReceiver(networkReceiver);
+ super.finalize();
+ }
+
+ /**
+ * Synchronization is only possible, if there is an active network connection.
+ * <p>
+ * This method respects the user preference "Sync on Wi-Fi only".
+ * <p>
+ * NoteServerSyncHelper observes changes in the network connection.
+ * The current state can be retrieved with this method.
+ *
+ * @return true if sync is possible, otherwise false.
+ */
+ public boolean isSyncPossible() {
+ return isSyncPossible;
+ }
+
+ public boolean isNetworkConnected() {
+ return networkConnected;
+ }
+
+ public boolean isSyncOnlyOnWifi() {
+ return syncOnlyOnWifi;
+ }
+
+ /**
+ * Adds a callback method to the NoteServerSyncHelper for the synchronization part push local changes to the server.
+ * All callbacks will be executed once the synchronization operations are done.
+ * After execution the callback will be deleted, so it has to be added again if it shall be
+ * executed the next time all synchronize operations are finished.
+ *
+ * @param callback Implementation of ISyncCallback, contains one method that shall be executed.
+ */
+ public void addCallbackPush(Account account, ISyncCallback callback) {
+ if (account == null) {
+ Log.i(TAG, "ssoAccount is null. Is this a local account?");
+ callback.onScheduled();
+ callback.onFinish();
+ } else {
+ if (!callbacksPush.containsKey(account.getId())) {
+ callbacksPush.put(account.getId(), new ArrayList<>());
+ }
+ Objects.requireNonNull(callbacksPush.get(account.getId())).add(callback);
+ }
+ }
+
+ /**
+ * Adds a callback method to the NoteServerSyncHelper for the synchronization part pull remote changes from the server.
+ * All callbacks will be executed once the synchronization operations are done.
+ * After execution the callback will be deleted, so it has to be added again if it shall be
+ * executed the next time all synchronize operations are finished.
+ *
+ * @param callback Implementation of ISyncCallback, contains one method that shall be executed.
+ */
+ public void addCallbackPull(Account account, ISyncCallback callback) {
+ if (account == null) {
+ Log.i(TAG, "ssoAccount is null. Is this a local account?");
+ callback.onScheduled();
+ callback.onFinish();
+ } else {
+ if (!callbacksPull.containsKey(account.getId())) {
+ callbacksPull.put(account.getId(), new ArrayList<>());
+ }
+ Objects.requireNonNull(callbacksPull.get(account.getId())).add(callback);
+ }
+ }
+
+ /**
+ * Schedules a synchronization and start it directly, if the network is connected and no
+ * synchronization is currently running.
+ *
+ * @param onlyLocalChanges Whether to only push local changes to the server or to also load the whole list of notes from the server.
+ */
+ public void scheduleSync(Account account, boolean onlyLocalChanges) {
+ if (account == null) {
+ Log.i(TAG, SingleSignOnAccount.class.getSimpleName() + " is null. Is this a local account?");
+ } else {
+ if (syncActive.get(account.getId()) == null) {
+ syncActive.put(account.getId(), false);
+ }
+ Log.d(TAG, "Sync requested (" + (onlyLocalChanges ? "onlyLocalChanges" : "full") + "; " + (Boolean.TRUE.equals(syncActive.get(account.getId())) ? "sync active" : "sync NOT active") + ") ...");
+ if (isSyncPossible() && (!Boolean.TRUE.equals(syncActive.get(account.getId())) || onlyLocalChanges)) {
+ try {
+ SingleSignOnAccount ssoAccount = AccountImporter.getSingleSignOnAccount(context, account.getAccountName());
+ Log.d(TAG, "... starting now");
+ final NotesClient notesClient = NotesClient.newInstance(account.getPreferredApiVersion(), context);
+ final NotesServerSyncTask syncTask = new NotesServerSyncTask(notesClient, db, account, ssoAccount, onlyLocalChanges) {
+ @Override
+ void onPreExecute() {
+ syncStatus.postValue(true);
+ if (!syncScheduled.containsKey(localAccount.getId()) || syncScheduled.get(localAccount.getId()) == null) {
+ syncScheduled.put(localAccount.getId(), false);
+ }
+ if (!onlyLocalChanges && Boolean.TRUE.equals(syncScheduled.get(localAccount.getId()))) {
+ syncScheduled.put(localAccount.getId(), false);
+ }
+ syncActive.put(localAccount.getId(), true);
+ }
+
+ @Override
+ void onPostExecute(SyncResultStatus status) {
+ for (Throwable e : exceptions) {
+ Log.e(TAG, e.getMessage(), e);
+ }
+ if (!status.pullSuccessful || !status.pushSuccessful) {
+ syncErrors.postValue(exceptions);
+ }
+ syncActive.put(localAccount.getId(), false);
+ // notify callbacks
+ if (callbacks.containsKey(localAccount.getId()) && callbacks.get(localAccount.getId()) != null) {
+ for (ISyncCallback callback : Objects.requireNonNull(callbacks.get(localAccount.getId()))) {
+ callback.onFinish();
+ }
+ }
+ db.notifyWidgets();
+ db.updateDynamicShortcuts(localAccount.getId());
+ // start next sync if scheduled meanwhile
+ if (syncScheduled.containsKey(localAccount.getId()) && syncScheduled.get(localAccount.getId()) != null && Boolean.TRUE.equals(syncScheduled.get(localAccount.getId()))) {
+ scheduleSync(localAccount, false);
+ }
+ syncStatus.postValue(false);
+ }
+ };
+ syncTask.addCallbacks(account, callbacksPush.get(account.getId()));
+ callbacksPush.put(account.getId(), new ArrayList<>());
+ if (!onlyLocalChanges) {
+ syncTask.addCallbacks(account, callbacksPull.get(account.getId()));
+ callbacksPull.put(account.getId(), new ArrayList<>());
+ }
+ executor.submit(syncTask);
+ } catch (NextcloudFilesAppAccountNotFoundException e) {
+ Log.e(TAG, "... Could not find " + SingleSignOnAccount.class.getSimpleName() + " for account name " + account.getAccountName());
+ e.printStackTrace();
+ }
+ } else if (!onlyLocalChanges) {
+ Log.d(TAG, "... scheduled");
+ syncScheduled.put(account.getId(), true);
+ if (callbacksPush.containsKey(account.getId()) && callbacksPush.get(account.getId()) != null) {
+ final List<ISyncCallback> callbacks = callbacksPush.get(account.getId());
+ if (callbacks != null) {
+ for (ISyncCallback callback : callbacks) {
+ callback.onScheduled();
+ }
+ } else {
+ Log.w(TAG, "List of push-callbacks was set for account \"" + account.getAccountName() + "\" but it was null");
+ }
+ }
+ } else {
+ Log.d(TAG, "... do nothing");
+ if (callbacksPush.containsKey(account.getId()) && callbacksPush.get(account.getId()) != null) {
+ final List<ISyncCallback> callbacks = callbacksPush.get(account.getId());
+ if (callbacks != null) {
+ for (ISyncCallback callback : callbacks) {
+ callback.onScheduled();
+ }
+ } else {
+ Log.w(TAG, "List of push-callbacks was set for account \"" + account.getAccountName() + "\" but it was null");
+ }
+ }
+ }
+ }
+ }
+
+ public void updateNetworkStatus() {
+ try {
+ final ConnectivityManager connMgr = (ConnectivityManager) context.getApplicationContext().getSystemService(Context.CONNECTIVITY_SERVICE);
+
+ if (connMgr == null) {
+ throw new NetworkErrorException("ConnectivityManager is null");
+ }
+
+ final NetworkInfo activeInfo = connMgr.getActiveNetworkInfo();
+
+ if (activeInfo == null) {
+ throw new NetworkErrorException("NetworkInfo is null");
+ }
+
+ if (activeInfo.isConnected()) {
+ networkConnected = true;
+
+ final NetworkInfo networkInfo = connMgr.getNetworkInfo((ConnectivityManager.TYPE_WIFI));
+
+ if (networkInfo == null) {
+ throw new NetworkErrorException("connMgr.getNetworkInfo(ConnectivityManager.TYPE_WIFI) is null");
+ }
+
+ isSyncPossible = !syncOnlyOnWifi || networkInfo.isConnected();
+
+ if (isSyncPossible) {
+ Log.d(TAG, "Network connection established.");
+ } else {
+ Log.d(TAG, "Network connected, but not used because only synced on wifi.");
+ }
+ } else {
+ networkConnected = false;
+ isSyncPossible = false;
+ Log.d(TAG, "No network connection.");
+ }
+ } catch (NetworkErrorException e) {
+ e.printStackTrace();
+ networkConnected = false;
+ isSyncPossible = false;
+ }
+ }
+
+ @NonNull
+ public LiveData<Boolean> getSyncStatus() {
+ return distinctUntilChanged(this.syncStatus);
+ }
+
+ @NonNull
+ public LiveData<ArrayList<Throwable>> getSyncErrors() {
+ return this.syncErrors;
+ }
+}
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
new file mode 100644
index 00000000..054e6a63
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesServerSyncTask.java
@@ -0,0 +1,232 @@
+package it.niedermann.owncloud.notes.persistence;
+
+import android.util.Log;
+
+import androidx.annotation.NonNull;
+
+import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException;
+import com.nextcloud.android.sso.exceptions.TokenMismatchException;
+import com.nextcloud.android.sso.model.SingleSignOnAccount;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+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.persistence.entity.Account;
+import it.niedermann.owncloud.notes.persistence.entity.Note;
+import it.niedermann.owncloud.notes.shared.model.DBStatus;
+import it.niedermann.owncloud.notes.shared.model.ISyncCallback;
+import it.niedermann.owncloud.notes.shared.model.ServerResponse;
+import it.niedermann.owncloud.notes.shared.model.SyncResultStatus;
+
+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;
+
+
+/**
+ * {@link NotesServerSyncTask} is a {@link Thread} which performs the synchronization in a background thread.
+ * Synchronization consists of two parts: {@link #pushLocalChanges()} and {@link #pullRemoteChanges}.
+ */
+abstract class NotesServerSyncTask extends Thread {
+
+ private static final String TAG = NotesServerSyncTask.class.getSimpleName();
+
+ @NonNull
+ private final NotesClient notesClient;
+ @NonNull
+ private final NotesDatabase db;
+ @NonNull
+ protected final Account localAccount;
+ @NonNull
+ private final SingleSignOnAccount ssoAccount;
+ private final boolean onlyLocalChanges;
+ @NonNull
+ protected final Map<Long, List<ISyncCallback>> callbacks = new HashMap<>();
+ @NonNull
+ protected final ArrayList<Throwable> exceptions = new ArrayList<>();
+
+ NotesServerSyncTask(@NonNull NotesClient notesClient, @NonNull NotesDatabase db, @NonNull Account localAccount, @NonNull SingleSignOnAccount ssoAccount, boolean onlyLocalChanges) {
+ super(TAG);
+ this.notesClient = notesClient;
+ this.db = db;
+ this.localAccount = localAccount;
+ this.ssoAccount = ssoAccount;
+ this.onlyLocalChanges = onlyLocalChanges;
+ }
+
+ void addCallbacks(Account account, List<ISyncCallback> callbacks) {
+ this.callbacks.put(account.getId(), callbacks);
+ }
+
+ @Override
+ public void run() {
+ onPreExecute();
+
+ Log.i(TAG, "STARTING SYNCHRONIZATION");
+ final SyncResultStatus status = new SyncResultStatus();
+ status.pushSuccessful = pushLocalChanges();
+ if (!onlyLocalChanges) {
+ status.pullSuccessful = pullRemoteChanges();
+ }
+ Log.i(TAG, "SYNCHRONIZATION FINISHED");
+
+ onPostExecute(status);
+ }
+
+ abstract void onPreExecute();
+
+ abstract void onPostExecute(SyncResultStatus status);
+
+ /**
+ * Push local changes: for each locally created/edited/deleted Note, use NotesClient in order to push the changed to the server.
+ */
+ private boolean pushLocalChanges() {
+ Log.d(TAG, "pushLocalChanges()");
+
+ boolean success = true;
+ final List<Note> notes = db.getNoteDao().getLocalModifiedNotes(localAccount.getId());
+ for (Note note : notes) {
+ Log.d(TAG, " Process Local Note: " + note);
+ try {
+ Note remoteNote;
+ switch (note.getStatus()) {
+ case LOCAL_EDITED:
+ Log.v(TAG, " ...create/edit");
+ if (note.getRemoteId() != null) {
+ Log.v(TAG, " ...Note has remoteId → try to edit");
+ try {
+ remoteNote = notesClient.editNote(ssoAccount, note).getNote();
+ } catch (NextcloudHttpRequestFailedException e) {
+ if (e.getStatusCode() == HTTP_NOT_FOUND) {
+ Log.v(TAG, " ...Note does no longer exist on server → recreate");
+ remoteNote = notesClient.createNote(ssoAccount, note).getNote();
+ } else {
+ throw e;
+ }
+ }
+ } else {
+ Log.v(TAG, " ...Note does not have a remoteId yet → create");
+ remoteNote = notesClient.createNote(ssoAccount, note).getNote();
+ db.getNoteDao().updateRemoteId(note.getId(), remoteNote.getRemoteId());
+ }
+ // Please note, that db.updateNote() realized an optimistic conflict resolution, which is required for parallel changes of this Note from the UI.
+ db.getNoteDao().updateIfNotModifiedLocallyDuringSync(note.getId(), remoteNote.getModified().getTimeInMillis(), remoteNote.getTitle(), remoteNote.getFavorite(), remoteNote.getETag(), remoteNote.getContent(), generateNoteExcerpt(remoteNote.getContent(), remoteNote.getTitle()), note.getContent(), note.getCategory(), note.getFavorite());
+ break;
+ case LOCAL_DELETED:
+ if (note.getRemoteId() == null) {
+ Log.v(TAG, " ...delete (only local, since it has never been synchronized)");
+ } else {
+ Log.v(TAG, " ...delete (from server and local)");
+ try {
+ notesClient.deleteNote(ssoAccount, note.getRemoteId());
+ } catch (NextcloudHttpRequestFailedException e) {
+ if (e.getStatusCode() == HTTP_NOT_FOUND) {
+ Log.v(TAG, " ...delete (note has already been deleted remotely)");
+ } else {
+ throw e;
+ }
+ }
+ }
+ // Please note, that db.deleteNote() realizes an optimistic conflict resolution, which is required for parallel changes of this Note from the UI.
+ db.getNoteDao().deleteByNoteId(note.getId(), LOCAL_DELETED);
+ break;
+ default:
+ throw new IllegalStateException("Unknown State of Note " + note + ": " + note.getStatus());
+ }
+ } catch (NextcloudHttpRequestFailedException e) {
+ if (e.getStatusCode() == HTTP_NOT_MODIFIED) {
+ Log.d(TAG, "Server returned HTTP Status Code 304 - Not Modified");
+ } else {
+ exceptions.add(e);
+ success = false;
+ }
+ } catch (Exception e) {
+ if (e instanceof TokenMismatchException) {
+ SSOClient.invalidateAPICache(ssoAccount);
+ }
+ exceptions.add(e);
+ success = false;
+ }
+ }
+ return success;
+ }
+
+ /**
+ * Pull remote Changes: update or create each remote note (if local pendant has no changes) and remove remotely deleted notes.
+ */
+ private boolean pullRemoteChanges() {
+ Log.d(TAG, "pullRemoteChanges() for account " + localAccount.getAccountName());
+ try {
+ final Map<Long, Long> idMap = db.getIdMap(localAccount.getId());
+ final Calendar modified = localAccount.getModified();
+ final long modifiedForServer = modified == null ? 0 : modified.getTimeInMillis() / 1_000;
+ final ServerResponse.NotesResponse response = notesClient.getNotes(ssoAccount, modifiedForServer, localAccount.getETag());
+ List<Note> remoteNotes = response.getNotes();
+ Set<Long> remoteIDs = new HashSet<>();
+ // pull remote changes: update or create each remote note
+ for (Note remoteNote : remoteNotes) {
+ Log.v(TAG, " Process Remote Note: " + remoteNote);
+ remoteIDs.add(remoteNote.getRemoteId());
+ if (remoteNote.getModified() == null) {
+ Log.v(TAG, " ... unchanged");
+ } else if (idMap.containsKey(remoteNote.getRemoteId())) {
+ Log.v(TAG, " ... found → Update");
+ Long localId = idMap.get(remoteNote.getRemoteId());
+ if (localId != null) {
+ db.getNoteDao().updateIfNotModifiedLocallyAndAnyRemoteColumnHasChanged(
+ localId, remoteNote.getModified().getTimeInMillis(), remoteNote.getTitle(), remoteNote.getFavorite(), remoteNote.getCategory(), remoteNote.getETag(), remoteNote.getContent(), generateNoteExcerpt(remoteNote.getContent(), remoteNote.getTitle()));
+ } else {
+ Log.e(TAG, "Tried to update note from server, but local id of note is null. " + remoteNote);
+ }
+ } else {
+ Log.v(TAG, " ... create");
+ db.addNote(localAccount.getId(), remoteNote);
+ }
+ }
+ Log.d(TAG, " Remove remotely deleted Notes (only those without local changes)");
+ // remove remotely deleted notes (only those without local changes)
+ for (Map.Entry<Long, Long> entry : idMap.entrySet()) {
+ if (!remoteIDs.contains(entry.getKey())) {
+ Log.v(TAG, " ... remove " + entry.getValue());
+ db.getNoteDao().deleteByNoteId(entry.getValue(), DBStatus.VOID);
+ }
+ }
+
+ // update ETag and Last-Modified in order to reduce size of next response
+ localAccount.setETag(response.getETag());
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTimeInMillis(response.getLastModified());
+ localAccount.setModified(calendar);
+ db.getAccountDao().updateETag(localAccount.getId(), localAccount.getETag());
+ db.getAccountDao().updateModified(localAccount.getId(), localAccount.getModified().getTimeInMillis());
+ try {
+ if (db.updateApiVersion(localAccount.getId(), response.getSupportedApiVersions())) {
+ localAccount.setApiVersion(response.getSupportedApiVersions());
+ }
+ } catch (Exception e) {
+ exceptions.add(e);
+ }
+ return true;
+ } catch (NextcloudHttpRequestFailedException e) {
+ Log.d(TAG, "Server returned HTTP Status Code " + e.getStatusCode() + " - " + e.getMessage());
+ if (e.getStatusCode() == HTTP_NOT_MODIFIED) {
+ return true;
+ } else {
+ exceptions.add(e);
+ return false;
+ }
+ } catch (Exception e) {
+ if (e instanceof TokenMismatchException) {
+ SSOClient.invalidateAPICache(ssoAccount);
+ }
+ exceptions.add(e);
+ return false;
+ }
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java
index 561ce8a4..2dfad616 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/NoteDao.java
@@ -10,7 +10,7 @@ import androidx.room.Update;
import java.util.List;
import java.util.Set;
-import it.niedermann.owncloud.notes.persistence.NoteServerSyncHelper;
+import it.niedermann.owncloud.notes.persistence.NotesServerSyncHelper;
import it.niedermann.owncloud.notes.persistence.entity.Account;
import it.niedermann.owncloud.notes.persistence.entity.CategoryWithNotesCount;
import it.niedermann.owncloud.notes.persistence.entity.Note;
@@ -169,7 +169,7 @@ public interface NoteDao {
void updateRemoteId(long id, Long remoteId);
/**
- * used by: {@link NoteServerSyncHelper.SyncTask#pushLocalChanges()} update only, if not modified locally during the synchronization
+ * used by: {@link NotesServerSyncHelper.SyncTask#pushLocalChanges()} update only, if not modified locally during the synchronization
* (i.e. all (!) user changeable columns (content, favorite, category) must still have the same value), uses reference value gathered at start of synchronization
*/
@Query("UPDATE NOTE SET title = :targetTitle, modified = :targetModified, favorite = :targetFavorite, etag = :targetETag, content = :targetContent, status = '', excerpt = :targetExcerpt " +
@@ -177,7 +177,7 @@ public interface NoteDao {
int updateIfNotModifiedLocallyDuringSync(long noteId, Long targetModified, String targetTitle, boolean targetFavorite, String targetETag, String targetContent, String targetExcerpt, String contentBeforeSyncStart, String categoryBeforeSyncStart, boolean favoriteBeforeSyncStart);
/**
- * used by: {@link NoteServerSyncHelper.SyncTask#pullRemoteChanges()} update only, if not modified locally (i.e. STATUS="") and if modified remotely (i.e. any (!) column has changed)
+ * used by: {@link NotesServerSyncHelper.SyncTask#pullRemoteChanges()} update only, if not modified locally (i.e. STATUS="") and if modified remotely (i.e. any (!) column has changed)
*/
@Query("UPDATE NOTE SET title = :title, modified = :modified, favorite = :favorite, etag = :eTag, content = :content, status = '', excerpt = :excerpt " +
"WHERE id = :id AND status = '' AND (title != :title OR modified != :modified OR favorite != :favorite OR category != :category OR (eTag IS NULL OR eTag != :eTag) OR content != :content)")