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); + } }