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

Commit bdb8eb97 authored by Fahim Salam Chowdhury's avatar Fahim Salam Chowdhury 👽
Browse files

1228-Remove_duplicate_remote_ids

parent 92844dd5
Loading
Loading
Loading
Loading
+2 −8
Original line number Diff line number Diff line
@@ -4,28 +4,20 @@ import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.O;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static it.niedermann.owncloud.notes.NotesApplication.isDarkThemeActive;
import static it.niedermann.owncloud.notes.NotesApplication.isGridViewEnabled;
import static it.niedermann.owncloud.notes.branding.BrandingUtil.getSecondaryForegroundColorDependingOnTheme;
import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.DEFAULT_CATEGORY;
import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.FAVORITES;
import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.RECENT;
import static it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType.UNCATEGORIZED;
import static it.niedermann.owncloud.notes.shared.util.NotesColorUtil.contrastRatioIsSufficient;
import static it.niedermann.owncloud.notes.shared.util.SSOUtil.askForNewAccount;

import android.accounts.NetworkErrorException;
import android.animation.AnimatorInflater;
import android.app.SearchManager;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;

import it.niedermann.owncloud.notes.importaccount.ImportMurenaAccountViewModel;
import trikita.log.Log;
import android.view.View;

import androidx.annotation.NonNull;
@@ -80,6 +72,7 @@ import it.niedermann.owncloud.notes.edit.category.CategoryViewModel;
import it.niedermann.owncloud.notes.exception.ExceptionDialogFragment;
import it.niedermann.owncloud.notes.exception.IntendedOfflineException;
import it.niedermann.owncloud.notes.importaccount.ImportAccountActivity;
import it.niedermann.owncloud.notes.importaccount.ImportMurenaAccountViewModel;
import it.niedermann.owncloud.notes.main.items.ItemAdapter;
import it.niedermann.owncloud.notes.main.items.grid.GridItemDecoration;
import it.niedermann.owncloud.notes.main.items.list.NotesListViewItemTouchHelper;
@@ -101,6 +94,7 @@ import it.niedermann.owncloud.notes.shared.model.NoteClickListener;
import it.niedermann.owncloud.notes.shared.util.CustomAppGlideModule;
import it.niedermann.owncloud.notes.shared.util.NoteUtil;
import it.niedermann.owncloud.notes.shared.util.ShareUtil;
import trikita.log.Log;

public class MainActivity extends LockedActivity implements NoteClickListener, AccountPickerListener, AccountSwitcherListener, CategoryDialogFragment.CategoryDialogListener {

+1 −2
Original line number Diff line number Diff line
@@ -20,7 +20,6 @@ import android.accounts.NetworkErrorException;
import android.app.Application;
import android.content.Context;
import android.text.TextUtils;
import trikita.log.Log;
import android.util.Pair;

import androidx.annotation.MainThread;
@@ -66,6 +65,7 @@ import it.niedermann.owncloud.notes.shared.model.IResponseCallback;
import it.niedermann.owncloud.notes.shared.model.ImportStatus;
import it.niedermann.owncloud.notes.shared.model.Item;
import it.niedermann.owncloud.notes.shared.model.NavigationCategory;
import trikita.log.Log;

public class MainViewModel extends AndroidViewModel {

@@ -667,5 +667,4 @@ public class MainViewModel extends AndroidViewModel {
    public void migrateOldNotes(@NonNull Account account) {
        repo.migrateOldNotesIfPossible(account.getId());
    }

}
 No newline at end of file
+124 −1
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@ import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.O;
import static androidx.lifecycle.Transformations.distinctUntilChanged;
import static androidx.lifecycle.Transformations.map;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toMap;
import static it.niedermann.owncloud.notes.edit.EditNoteActivity.ACTION_SHORTCUT;
import static it.niedermann.owncloud.notes.shared.util.NoteUtil.generateNoteExcerpt;
@@ -43,9 +44,11 @@ import com.nextcloud.android.sso.model.SingleSignOnAccount;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -81,6 +84,7 @@ import retrofit2.Call;
public class NotesRepository {

    private static final String PREF_KEY_MIGRATION_DONE = "old_note_migration_done";
    private static final String PREF_KEY_REMOTE_CONFLICT_RESOLVED = "remote_conflict_resolved";

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

@@ -465,6 +469,15 @@ public class NotesRepository {
                .collect(toMap(Note::getRemoteId, Note::getId));
    }

    @NonNull
    @WorkerThread
    public Map<Long, List<Note>> getNoteMapByRemoteId(long accountId) {
        return db.getNoteDao()
                .getActiveNoteList(accountId)
                .stream()
                .collect(groupingBy(Note::getRemoteId));
    }

    @AnyThread
    public void toggleFavoriteAndSync(Account account, long noteId) {
        executor.submit(() -> {
@@ -1005,7 +1018,7 @@ public class NotesRepository {
    }

    private boolean isNoteAlreadyExistsInNewTable(@NonNull OldNote oldNote) {
        return db.getNoteDao().countByTitleAndContent(oldNote.getTitle(), oldNote.getContent()) > 0;
        return db.getNoteDao().countByTitleAndContent(oldNote.getTitle(), oldNote.getContent(), oldNote.getRemoteId()) > 0;
    }

    private void addNewNote(long accountId, @NonNull OldNote oldNote) {
@@ -1026,4 +1039,114 @@ public class NotesRepository {
                .putBoolean(PREF_KEY_MIGRATION_DONE, true)
                .apply();
    }

    private boolean isRemoteIdConflictResolved() {
        final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
        return sharedPreferences.getBoolean(PREF_KEY_REMOTE_CONFLICT_RESOLVED, false);
    }

    private void setRemoteIdConflictResolved() {
        final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
        sharedPreferences.edit()
                .putBoolean(PREF_KEY_REMOTE_CONFLICT_RESOLVED, true)
                .apply();
    }

    @WorkerThread
    public void removeDuplicateRemoteIds() {
        if (isRemoteIdConflictResolved()) {
            return;
        }

        List<Account> accounts = getAccounts();

        for (Account account : accounts) {
            if (account == null) {
                continue;
            }

            removeDuplicateNotesForAccount(account);
        }

        setRemoteIdConflictResolved();
    }

    @WorkerThread
    private void removeDuplicateNotesForAccount(@NonNull Account account) {
        Map<Long, List<Note>> noteMap = getNoteMapByRemoteId(account.getId());

        for (Map.Entry<Long, List<Note>> mapEntry : noteMap.entrySet()) {
            if (doNotHaveDuplicateNote(mapEntry)) {
                continue;
            }

            removeConflictedNotes(mapEntry.getValue());
        }
    }

    private boolean doNotHaveDuplicateNote(@NonNull Map.Entry<Long, List<Note>> mapEntry) {
        return mapEntry.getValue().size() <= 1;
    }

    /*
    * Preserve the first note & remove the duplicates.
    * If notes have different content texts, concat them in the preserve note, so user will not loss any content.
     */
    @WorkerThread
    private void removeConflictedNotes(@NonNull List<Note> noteList) {
        Note preservedNote = noteList.get(0);

        Set<String> contentSet = new HashSet<>();
        contentSet.add(preservedNote.getContent());

        int contentCounter = 1;

        for (int i = 1; i < noteList.size(); i++) {
            Note note = noteList.get(i);

            db.getNoteDao().deleteByNoteId(note.getId(), note.getStatus());

            if (contentSet.contains(note.getContent())) {
                continue;
            }

            contentSet.add(note.getContent());
            contentCounter = updatePreservedNoteContent(contentCounter, preservedNote, note);
        }

        updateNoteForConflict(contentCounter - 1, preservedNote);
    }

    private int updatePreservedNoteContent(int position, @NonNull Note preservedNote, @NonNull Note note) {
        String content = getPreviousContentForConflict(position, preservedNote);
        content += getConflictedContent(++position, note);
        preservedNote.setContent(content);
        return position;
    }

    @NonNull
    private String getConflictedContent(int position, @NonNull Note note) {
        String modified = note.getModified().getTime().toString();
        return context.getString(R.string.conflicted_note_content_title, position, modified)
                + note.getContent();
    }

    @NonNull
    private String getPreviousContentForConflict(int position, @NonNull Note note) {
        if (position > 1) {
            return note.getContent();
        }

        return context.getString(R.string.conflict_note_detected)
                + getConflictedContent(position, note);
    }

    @WorkerThread
    private void updateNoteForConflict(int numberOfConflicts, @NonNull Note note) {
        if (numberOfConflicts > 0) {
            note.setStatus(DBStatus.LOCAL_EDITED);
            note.setModified(Calendar.getInstance());
            db.getNoteDao().updateNote(note);
        }
    }
}
+2 −0
Original line number Diff line number Diff line
@@ -205,6 +205,8 @@ abstract class NotesServerSyncTask extends Thread {
    private boolean pullRemoteChanges() {
        Log.d(TAG, "pullRemoteChanges() for account " + localAccount.getAccountName());
        try {
            repo.removeDuplicateRemoteIds();

            final var idMap = repo.getIdMap(localAccount.getId());

            // FIXME re-reading the localAccount is only a workaround for a not-up-to-date eTag in localAccount.
+5 −2
Original line number Diff line number Diff line
@@ -138,6 +138,9 @@ public interface NoteDao {
    @Query("SELECT id, remoteId, 0 as accountId, '' as title, 0 as favorite, '' as excerpt, 0 as modified, '' as eTag, 0 as status, '' as category, '' as content, 0 as scrollY  FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND remoteId IS NOT NULL")
    List<Note> getRemoteIdAndId(long accountId);

    @Query("SELECT * FROM NOTE WHERE accountId = :accountId AND status != 'LOCAL_DELETED' AND remoteId IS NOT NULL")
    List<Note> getActiveNoteList(long accountId);

    /**
     * Get a single {@link Note} by {@link Note#remoteId} (aka. Nextcloud file id)
     *
@@ -194,6 +197,6 @@ public interface NoteDao {
    @Query("SELECT COUNT(*) FROM NOTE WHERE STATUS != '' AND accountId = :accountId")
    Long countUnsynchronizedNotes(long accountId);

    @Query("SELECT COUNT(*) FROM NOTE WHERE title = :title AND content = :content")
    Long countByTitleAndContent(String title, String content);
    @Query("SELECT COUNT(*) FROM NOTE WHERE (title = :title AND content = :content) OR remoteId = :remoteId")
    Long countByTitleAndContent(String title, String content, long remoteId);
}
Loading