diff --git a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java
index c1d4a119db2c0673cae304191c2ee48ad48123c4..51ab9eeeffbab1ba192d186fa8bc4572e6412cb6 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/main/MainActivity.java
@@ -175,6 +175,8 @@ public class MainActivity extends LockedActivity implements NoteClickListener, A
executor.submit(() -> {
try {
final var account = mainViewModel.getLocalAccountByAccountName(SingleAccountHelper.getCurrentSingleSignOnAccount(getApplicationContext()).name);
+ mainViewModel.migrateOldNotes(account);
+
runOnUiThread(() -> mainViewModel.postCurrentAccount(account));
} catch (NextcloudFilesAppAccountNotFoundException e) {
// Verbose log output for https://github.com/stefan-niedermann/nextcloud-notes/issues/1256
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 5ce45bd3e492a73f7fdcdfc383f4fe73f7f0cae9..601e891712451f04c305dcfdb882933319396863 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
@@ -663,4 +663,9 @@ public class MainViewModel extends AndroidViewModel {
final var lower = input.toLowerCase(Locale.ROOT);
return lower.contains("failed to connect") && lower.contains("network is unreachable");
}
+
+ public void migrateOldNotes(@NonNull Account account) {
+ repo.migrateOldNotesIfPossible(account.getId());
+ }
+
}
\ No newline at end of file
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 ac9898c1e10254e3dab0c11151bd6e95600ec810..db7d7ded55cfdca7eb4ef3fc444b0760098de72b 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
@@ -1,7 +1,6 @@
package it.niedermann.owncloud.notes.persistence;
import android.content.Context;
-import trikita.log.Log;
import androidx.annotation.NonNull;
import androidx.room.Database;
@@ -10,6 +9,8 @@ import androidx.room.RoomDatabase;
import androidx.room.TypeConverters;
import androidx.sqlite.db.SupportSQLiteDatabase;
+import java.util.List;
+
import it.niedermann.owncloud.notes.persistence.dao.AccountDao;
import it.niedermann.owncloud.notes.persistence.dao.CategoryOptionsDao;
import it.niedermann.owncloud.notes.persistence.dao.NoteDao;
@@ -35,6 +36,7 @@ import it.niedermann.owncloud.notes.persistence.migration.Migration_20_21;
import it.niedermann.owncloud.notes.persistence.migration.Migration_21_22;
import it.niedermann.owncloud.notes.persistence.migration.Migration_22_23;
import it.niedermann.owncloud.notes.persistence.migration.Migration_9_10;
+import trikita.log.Log;
@Database(
entities = {
@@ -61,9 +63,9 @@ public abstract class NotesDatabase extends RoomDatabase {
private static NotesDatabase create(final Context context) {
return Room.databaseBuilder(
- context,
- NotesDatabase.class,
- NOTES_DB_NAME)
+ context,
+ NotesDatabase.class,
+ NOTES_DB_NAME)
.addMigrations(
new Migration_9_10(), // v2.0.0
new Migration_10_11(context),
@@ -105,4 +107,17 @@ public abstract class NotesDatabase extends RoomDatabase {
public abstract WidgetSingleNoteDao getWidgetSingleNoteDao();
public abstract WidgetNotesListDao getWidgetNotesListDao();
+
+ /**
+ * retrieve notes from old `NOTES` table for migration.
+ * If we try to follow the *ROOM way* (entity, dao) for `NOTES` table, the existing data will be removed.
+ * So we have to retrieve old entries via raw sql commands.
+ *
+ * Check for more details
+ * @return list of old notes entries
+ */
+ @NonNull
+ public List getNotesFromOldTable() {
+ return OldNoteRetriever.getNotesFromOldTable(this);
+ }
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java
index 9770197b931bb6b0191e3a06035cb57553d7b9a2..e99e88916232b49cc44155fd025b31eafa90eb76 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java
@@ -21,6 +21,7 @@ import android.content.pm.ShortcutManager;
import android.graphics.drawable.Icon;
import android.net.ConnectivityManager;
import android.text.TextUtils;
+
import trikita.log.Log;
import androidx.annotation.AnyThread;
@@ -79,6 +80,8 @@ import retrofit2.Call;
@SuppressWarnings("UnusedReturnValue")
public class NotesRepository {
+ private static final String PREF_KEY_MIGRATION_DONE = "old_note_migration_done";
+
private static final String TAG = NotesRepository.class.getSimpleName();
private static NotesRepository instance;
@@ -958,4 +961,68 @@ public class NotesRepository {
public void updateDisplayName(long id, @Nullable String displayName) {
db.getAccountDao().updateDisplayName(id, displayName);
}
+
+ /**
+ * migrate old notes to latest note table.
+ * if the migration is already done, skip.
+ * link the provided account to the migrated notes, as old notes don't have account entry to them.
+ *
+ * * Check for more details.
+ * @param accountId which will be used to link migrated notes to account
+ */
+ @AnyThread
+ public void migrateOldNotesIfPossible(long accountId) {
+ if (isMigrationDone()) {
+ return;
+ }
+
+ migrateOldNotes(accountId);
+ }
+
+ private void migrateOldNotes(long accountId) {
+ try {
+ executor.submit(() -> {
+ List oldNotes = db.getNotesFromOldTable();
+ if (oldNotes.isEmpty()) {
+ updatePrefOnMigrationDone();
+ return;
+ }
+
+ for (OldNote oldNote : oldNotes) {
+ if (oldNote == null || isNoteAlreadyExistsInNewTable(oldNote)) {
+ continue;
+ }
+
+ addNewNote(accountId, oldNote);
+ }
+
+ updatePrefOnMigrationDone();
+ });
+ } catch (Exception e) {
+ Log.e(TAG, "An exception has been caught", e);
+ }
+ }
+
+ private boolean isNoteAlreadyExistsInNewTable(@NonNull OldNote oldNote) {
+ return db.getNoteDao().countByTitleAndContent(oldNote.getTitle(), oldNote.getContent()) > 0;
+ }
+
+ private void addNewNote(long accountId, @NonNull OldNote oldNote) {
+ Note newNote = new Note(oldNote.getRemoteId(), oldNote.getModified(), oldNote.getTitle(), oldNote.getContent(), oldNote.getCategory(), oldNote.getFavorite(), oldNote.getETag());
+ newNote.setStatus(oldNote.getStatus());
+ newNote.setAccountId(accountId);
+ db.getNoteDao().addNote(newNote);
+ }
+
+ private boolean isMigrationDone() {
+ final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
+ return sharedPreferences.getBoolean(PREF_KEY_MIGRATION_DONE, false);
+ }
+
+ private void updatePrefOnMigrationDone() {
+ final var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());
+ sharedPreferences.edit()
+ .putBoolean(PREF_KEY_MIGRATION_DONE, true)
+ .apply();
+ }
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/OldNote.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/OldNote.java
new file mode 100644
index 0000000000000000000000000000000000000000..2da73b7aa55e0e2133f183973426042646cc7a5a
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/OldNote.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright MURENA SAS 2023
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package it.niedermann.owncloud.notes.persistence;
+
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.Serializable;
+import java.util.Calendar;
+
+import it.niedermann.owncloud.notes.shared.model.DBStatus;
+
+/**
+ * Entity representation for `NOTES` table.
+ * This is added to migrate old `NOTES` items to new `Note` table.
+ *
+ * Check for more details
+ */
+public class OldNote implements Serializable {
+
+ private long id;
+
+ private long remoteId;
+
+ @NonNull
+ private DBStatus status;
+
+ @NonNull
+ private String title;
+
+ @Nullable
+ private Calendar modified;
+
+ @NonNull
+ private String content;
+
+ private boolean favorite = false;
+
+ @NonNull
+ private String category = "";
+
+ @Nullable
+ private String eTag;
+
+ public OldNote(long id, long remoteId, @Nullable Calendar modified, @NonNull String title, @NonNull String content, boolean favorite, @NonNull String category, @Nullable String eTag, @NonNull DBStatus status) {
+ this.id = id;
+ this.remoteId = remoteId;
+ this.status = status;
+ this.title = title;
+ this.modified = modified;
+ this.content = content;
+ this.favorite = favorite;
+ this.category = category;
+ this.eTag = eTag;
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ public long getRemoteId() {
+ return remoteId;
+ }
+
+ public void setRemoteId(long remoteId) {
+ this.remoteId = remoteId;
+ }
+
+ @NonNull
+ public DBStatus getStatus() {
+ return status;
+ }
+
+ public void setStatus(@NonNull DBStatus status) {
+ this.status = status;
+ }
+
+ @NonNull
+ public String getTitle() {
+ return title;
+ }
+
+ public void setTitle(@NonNull String title) {
+ this.title = title;
+ }
+
+ @Nullable
+ public Calendar getModified() {
+ return modified;
+ }
+
+ public void setModified(@Nullable Calendar modified) {
+ this.modified = modified;
+ }
+
+ @NonNull
+ public String getContent() {
+ return content;
+ }
+
+ public void setContent(@NonNull String content) {
+ this.content = content;
+ }
+
+ public boolean getFavorite() {
+ return favorite;
+ }
+
+ public void setFavorite(boolean favorite) {
+ this.favorite = favorite;
+ }
+
+ @NonNull
+ public String getCategory() {
+ return category;
+ }
+
+ public void setCategory(@NonNull String category) {
+ this.category = category;
+ }
+
+ @Nullable
+ public String getETag() {
+ return eTag;
+ }
+
+ public void setETag(@Nullable String eTag) {
+ this.eTag = eTag;
+ }
+}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/OldNoteRetriever.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/OldNoteRetriever.java
new file mode 100644
index 0000000000000000000000000000000000000000..2e2e6a7d2276494083a374ad84b9d28c2793e085
--- /dev/null
+++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/OldNoteRetriever.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright MURENA SAS 2023
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package it.niedermann.owncloud.notes.persistence;
+
+import android.database.Cursor;
+
+import androidx.annotation.NonNull;
+import androidx.room.RoomDatabase;
+import androidx.sqlite.db.SupportSQLiteDatabase;
+
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.Collections;
+import java.util.List;
+
+import it.niedermann.owncloud.notes.shared.model.DBStatus;
+
+/**
+ * This is a helper class which loads all notes from old table.
+ * Because of massive version jump, not-sync notes are not migrated to latest `Note` table from old `NOTES` table.
+ * This helper class load the notes from the `NOTES` table for migration.
+ *
+ * Check for more details
+ */
+public final class OldNoteRetriever {
+
+ private static final String OLD_NOTES_TABLE_NAME = "NOTES";
+
+ private OldNoteRetriever() {
+ }
+
+ @NonNull
+ public static List getNotesFromOldTable(@NonNull RoomDatabase database) {
+ final SupportSQLiteDatabase supportSQLiteDatabase = database.getOpenHelper().getWritableDatabase();
+
+ if (!isOldNoteTableExists(supportSQLiteDatabase)) { // old notes table can not be existed for fresh install
+ return Collections.emptyList();
+ }
+
+ return retrieveOldNotes(supportSQLiteDatabase);
+ }
+
+ @NonNull
+ private static List retrieveOldNotes(@NonNull SupportSQLiteDatabase supportSQLiteDatabase) {
+ final Cursor oldNotesCursor = supportSQLiteDatabase.query("SELECT * FROM " + OLD_NOTES_TABLE_NAME);
+ List notes = new ArrayList<>();
+
+ while (oldNotesCursor.moveToNext()) {
+ notes.add(getOldNoteFromCursor(oldNotesCursor));
+ }
+
+ oldNotesCursor.close();
+ return notes;
+ }
+
+ private static boolean isOldNoteTableExists(@NonNull SupportSQLiteDatabase supportSQLiteDatabase) {
+ final Cursor tableExistCursor = supportSQLiteDatabase.query("SELECT name FROM sqlite_master WHERE type='table' AND name='" + OLD_NOTES_TABLE_NAME + "'");
+ final boolean tableExists = tableExistCursor.moveToNext();
+ tableExistCursor.close();
+ return tableExists;
+ }
+
+ @NonNull
+ private static OldNote getOldNoteFromCursor(@NonNull Cursor cursor) {
+ final Calendar modified = Calendar.getInstance();
+ modified.setTimeInMillis(cursor.getLong(4) * 1000);
+ return new OldNote(cursor.getLong(0), cursor.getLong(1), modified, cursor.getString(3), cursor.getString(5), cursor.getInt(6) > 0, cursor.getString(7), cursor.getString(8), DBStatus.parse(cursor.getString(2)));
+ }
+}
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 cd48b5d80262c66129df5add73a7e96296bf9ce3..ce1475ccf44b1bf73086ffba0864b4e16c7e2c4c 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
@@ -193,4 +193,7 @@ 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);
}
diff --git a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/DBStatus.java b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/DBStatus.java
index ef06a277d2661c6355ad946f2c3e95250d2cee5b..26c204473b14a359f9631c9846b5b52505328c93 100644
--- a/app/src/main/java/it/niedermann/owncloud/notes/shared/model/DBStatus.java
+++ b/app/src/main/java/it/niedermann/owncloud/notes/shared/model/DBStatus.java
@@ -1,6 +1,7 @@
package it.niedermann.owncloud.notes.shared.model;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
/**
* Helps to distinguish between different local change types for Server Synchronization.
@@ -37,4 +38,18 @@ public enum DBStatus {
DBStatus(@NonNull String title) {
this.title = title;
}
+
+ /**
+ * Parse a String an get the appropriate DBStatus enum element.
+ *
+ * @param str The String containing the DBStatus identifier.
+ * @return The DBStatus fitting to the String.
+ */
+ public static DBStatus parse(@Nullable String str) {
+ if (str == null || str.isEmpty()) {
+ return DBStatus.VOID;
+ }
+
+ return DBStatus.valueOf(str);
+ }
}