Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e9fc505a authored by Stefan Niedermann's avatar Stefan Niedermann
Browse files

Get rid of deprecated AsyncTask

parent 4f8a9b42
Loading
Loading
Loading
Loading
+2 −2
Original line number Diff line number Diff line
@@ -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();
                }
+3 −3
Original line number Diff line number Diff line
@@ -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;
    }

+346 −0
Original line number Diff line number Diff line
@@ -8,7 +8,6 @@ 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;
@@ -18,49 +17,43 @@ 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 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.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 {
public class NotesServerSyncHelper {

    private static final String TAG = NoteServerSyncHelper.class.getSimpleName();
    private static final String TAG = NotesServerSyncHelper.class.getSimpleName();

    private static NoteServerSyncHelper instance;
    private final ExecutorService executor = Executors.newSingleThreadExecutor();

    private static NotesServerSyncHelper instance;

    private final NotesDatabase db;
    private final Context context;

    // Track network connection changes using a BroadcastReceiver
    /**
     * Track network connection changes using a {@link BroadcastReceiver}
     */
    private boolean isSyncPossible = false;
    private boolean networkConnected = false;
    private String syncOnlyOnWifiKey;
@@ -103,7 +96,7 @@ public class NoteServerSyncHelper {
    private final Map<Long, List<ISyncCallback>> callbacksPush = new HashMap<>();
    private final Map<Long, List<ISyncCallback>> callbacksPull = new HashMap<>();

    private NoteServerSyncHelper(NotesDatabase db) {
    private NotesServerSyncHelper(NotesDatabase db) {
        this.db = db;
        this.context = db.getContext();
        this.syncOnlyOnWifiKey = context.getApplicationContext().getResources().getString(R.string.pref_key_wifi_only);
@@ -126,9 +119,9 @@ public class NoteServerSyncHelper {
     * @param db {@link NotesDatabase}
     * @return NoteServerSyncHelper
     */
    public static synchronized NoteServerSyncHelper getInstance(NotesDatabase db) {
    public static synchronized NotesServerSyncHelper getInstance(NotesDatabase db) {
        if (instance == null) {
            instance = new NoteServerSyncHelper(db);
            instance = new NotesServerSyncHelper(db);
        }
        return instance;
    }
@@ -222,14 +215,50 @@ public class NoteServerSyncHelper {
                    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);
                    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<>());
                    }
                    syncTask.execute();
                    executor.submit(syncTask);
                } catch (NextcloudFilesAppAccountNotFoundException e) {
                    Log.e(TAG, "... Could not find " + SingleSignOnAccount.class.getSimpleName() + " for account name " + account.getAccountName());
                    e.printStackTrace();
@@ -305,237 +334,12 @@ public class NoteServerSyncHelper {
        }
    }

    /**
     * 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);
    }

    @NonNull
    public LiveData<ArrayList<Throwable>> getSyncErrors() {
        return this.syncErrors;
    }
+232 −0

File added.

Preview size limit exceeded, changes collapsed.

+3 −3
Original line number Diff line number Diff line
@@ -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)")
Loading