Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: Export and import app settings to/from note #526

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.orgzly.android.data

import com.orgzly.android.OrgzlyTest
import com.orgzly.android.prefs.AppPreferences
import junit.framework.TestCase
import org.junit.Assert
import org.junit.Test
import java.io.IOException

class DataRepositoryTest : OrgzlyTest() {

/**
* If the user attempts to export app settings to a note with a non-unique "ID" value, then
* - no export should happen
* - a runtime exception should be thrown
*/
@Test(expected = RuntimeException::class)
fun testExportSettingsAndSearchesToSelectedNote() {
testUtils.setupBook(
"book1",
"""
* Note 1
:PROPERTIES:
:ID: not-unique-value
:END:

content

* Note 2
:PROPERTIES:
:ID: not-unique-value
:END:

content

""".trimIndent()
)
TestCase.assertEquals(2, dataRepository.getNotes("book1").size)
AppPreferences.settingsExportAndImportNoteId(context, "not-unique-value")
try {
dataRepository.exportSettingsAndSearchesToSelectedNote()
} catch (e: IOException) {
Assert.assertTrue(e.message!!.contains("Found multiple"))
throw e
} finally {
TestCase.assertEquals("content", dataRepository.getNotes("book1")[0].note.content)
TestCase.assertEquals("content", dataRepository.getNotes("book1")[1].note.content)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.junit.Assume.assumeTrue;

Expand Down Expand Up @@ -176,7 +176,8 @@ public void testBookSinglePropertyIsParsed() {
""";
TestedBook testedBook = onBook(content);
testedBook.onLoad().isWhenSaved(content);
assertEquals(testedBook.book, dataRepository.findNoteOrBookHavingProperty("foo", "bar"));
assertEquals(new ArrayList<>(Collections.singleton(testedBook.book)),
dataRepository.findNotesOrBooksHavingProperty("foo", "bar"));
}

/**
Expand All @@ -197,8 +198,8 @@ public void testBookMultiplePropertiesAreParsed() {
""";
TestedBook testedBook = onBook(content);
testedBook.onLoad().isWhenSaved(content);
assertEquals(testedBook.book, dataRepository.findNoteOrBookHavingProperty("foo", "bar"));
assertEquals(testedBook.book, dataRepository.findNoteOrBookHavingProperty("bar", "foo"));
assertEquals(new ArrayList<>(Collections.singleton(testedBook.book)), dataRepository.findNotesOrBooksHavingProperty("foo", "bar"));
assertEquals(new ArrayList<>(Collections.singleton(testedBook.book)), dataRepository.findNotesOrBooksHavingProperty("bar", "foo"));
}

/**
Expand All @@ -220,12 +221,12 @@ public void testBookDuplicateProperties() {
""";
TestedBook testedBook = onBook(content);
testedBook.onLoad().isWhenSaved(content);
assertEquals(testedBook.book, dataRepository.findNoteOrBookHavingProperty("foo",
"secondvalue"));
assertEquals(testedBook.book, dataRepository.findNoteOrBookHavingProperty("bar",
"secondvalue"));
assertNull(dataRepository.findNoteOrBookHavingProperty("foo", "firstvalue"));
assertNull(dataRepository.findNoteOrBookHavingProperty("bar", "firstvalue"));
assertEquals(new ArrayList<>(Collections.singleton(testedBook.book)),
dataRepository.findNotesOrBooksHavingProperty("foo", "secondvalue"));
assertEquals(new ArrayList<>(Collections.singleton(testedBook.book)),
dataRepository.findNotesOrBooksHavingProperty("bar", "secondvalue"));
assertEquals(0, dataRepository.findNotesOrBooksHavingProperty("foo", "firstvalue").size());
assertEquals(0, dataRepository.findNotesOrBooksHavingProperty("bar", "firstvalue").size());
}

@Test
Expand All @@ -243,16 +244,16 @@ public void testBookDuplicatePropertiesDifferentCase() {
""";
TestedBook testedBook = onBook(content);
testedBook.onLoad().isWhenSaved(content);
assertEquals(testedBook.book, dataRepository.findNoteOrBookHavingProperty("foo",
"secondvalue"));
assertEquals(testedBook.book, dataRepository.findNoteOrBookHavingProperty("bar",
"secondvalue"));
assertEquals(testedBook.book, dataRepository.findNoteOrBookHavingProperty("FOO",
"secondvalue"));
assertEquals(testedBook.book, dataRepository.findNoteOrBookHavingProperty("BAR",
"secondvalue"));
assertNull(dataRepository.findNoteOrBookHavingProperty("foo", "firstvalue"));
assertNull(dataRepository.findNoteOrBookHavingProperty("bar", "firstvalue"));
assertEquals(new ArrayList<>(Collections.singleton(testedBook.book)),
dataRepository.findNotesOrBooksHavingProperty("foo", "secondvalue"));
assertEquals(new ArrayList<>(Collections.singleton(testedBook.book)),
dataRepository.findNotesOrBooksHavingProperty("bar", "secondvalue"));
assertEquals(new ArrayList<>(Collections.singleton(testedBook.book)),
dataRepository.findNotesOrBooksHavingProperty("FOO", "secondvalue"));
assertEquals(new ArrayList<>(Collections.singleton(testedBook.book)),
dataRepository.findNotesOrBooksHavingProperty("BAR", "secondvalue"));
assertEquals(0, dataRepository.findNotesOrBooksHavingProperty("foo", "firstvalue").size());
assertEquals(0, dataRepository.findNotesOrBooksHavingProperty("bar", "firstvalue").size());
}

/*
Expand Down
64 changes: 58 additions & 6 deletions app/src/main/java/com/orgzly/android/data/DataRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,19 @@ import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.map
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.preference.PreferenceManager
import androidx.sqlite.db.SupportSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQueryBuilder
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.orgzly.BuildConfig
import com.orgzly.R
import com.orgzly.android.*
import com.orgzly.android.data.mappers.OrgMapper
import com.orgzly.android.db.NotesClipboard
import com.orgzly.android.db.OrgzlyDatabase
import com.orgzly.android.db.dao.NoteDao
import com.orgzly.android.db.dao.NoteDao.NoteIdBookId
import com.orgzly.android.db.dao.NoteViewDao
import com.orgzly.android.db.dao.ReminderTimeDao
import com.orgzly.android.db.entity.*
Expand Down Expand Up @@ -1987,15 +1991,63 @@ class DataRepository @Inject constructor(
NotesOrgExporter(this).exportBook(book, writer)
}

fun findNoteHavingProperty(name: String, value: String): NoteDao.NoteIdBookId? {
return db.note().firstNoteHavingPropertyLowerCase(name.lowercase(), value.lowercase())
fun findNotesHavingProperty(name: String, value: String): List<NoteIdBookId> {
return db.note().allNotesHavingPropertyLowerCase(name.lowercase(), value.lowercase())
}

fun findNoteOrBookHavingProperty(name: String, value: String): Any? {
val foundNote = findNoteHavingProperty(name, value)
if (foundNote != null)
fun findNotesOrBooksHavingProperty(name: String, value: String): List<Any?> {
val foundNote = findNotesHavingProperty(name, value)
if (foundNote.isNotEmpty())
return foundNote
return db.book().firstBookHavingPropertyLowerCase(name.lowercase(), value.lowercase())
return db.book().allBooksHavingPropertyLowerCase(name.lowercase(), value.lowercase())
}

fun findUniqueNoteHavingProperty(name: String, value: String): NoteIdBookId? {
val foundNotes = db.note().allNotesHavingPropertyLowerCase(name.lowercase(), value.lowercase())
return if (foundNotes.isEmpty()) null
else if (foundNotes.size == 1) foundNotes[0]
else {
val msg = App.getAppContext().getString(R.string.error_multiple_notes_with_matching_property_value, name, value)
throw RuntimeException(msg)
}
}

fun exportSettingsAndSearchesToSelectedNote() {
val targetNote = findUniqueNoteHavingProperty("ID", AppPreferences.settingsExportAndImportNoteId(context))
if (targetNote != null) {
val gson = Gson()
// Get settings as JSON
val settingsJsonObject = gson.toJsonTree(PreferenceManager.getDefaultSharedPreferences(context).all)
// Get saved searches as JSON
val savedSearchesJsonObject = gson.fromJson("{}", JsonObject::class.java)
getSavedSearches().forEach {
savedSearchesJsonObject.addProperty(it.name, it.query)
}
// Put them together
val finalMap = mapOf("settings" to settingsJsonObject, "saved_searches" to savedSearchesJsonObject)
val finalJsonString = gson.toJson(finalMap)
updateNoteContent(targetNote.bookId, targetNote.noteId, finalJsonString)
}
}

fun importSettingsAndSearchesFromSelectedNote() {
val sourceNote = findUniqueNoteHavingProperty("ID", AppPreferences.settingsExportAndImportNoteId(context))
if (sourceNote != null) {
val notePayload = getNotePayload(sourceNote.noteId)
if (notePayload != null) {
val gson = Gson().fromJson(notePayload.content, Map::class.java)
val settings = gson["settings"] as Map<String, *>
if (settings.isNotEmpty())
AppPreferences.setDefaultPrefsFromJsonMap(context, settings)
val savedSearches: List<SavedSearch> = (gson["saved_searches"] as Map<String, String>)
.entries
.mapIndexed { index, entry ->
SavedSearch(0, entry.key, entry.value, index + 1)
}
if (savedSearches.isNotEmpty())
replaceSavedSearches(savedSearches)
}
}
}

/*
Expand Down
3 changes: 1 addition & 2 deletions app/src/main/java/com/orgzly/android/db/dao/BookDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,8 @@ abstract class BookDao : BaseDao<Book> {
FROM book_properties
LEFT JOIN books ON (books.id = book_properties.book_id)
WHERE LOWER(book_properties.name) = :name AND LOWER(book_properties.value) = :value AND books.id IS NOT NULL
LIMIT 1
""")
abstract fun firstBookHavingPropertyLowerCase(name: String, value: String): Book?
abstract fun allBooksHavingPropertyLowerCase(name: String, value: String): List<Book>

fun getOrInsert(name: String): Long =
get(name).let {
Expand Down
3 changes: 1 addition & 2 deletions app/src/main/java/com/orgzly/android/db/dao/NoteDao.kt
Original file line number Diff line number Diff line change
Expand Up @@ -306,9 +306,8 @@ abstract class NoteDao : BaseDao<Note> {
LEFT JOIN notes ON (notes.id = note_properties.note_id)
WHERE LOWER(note_properties.name) = :name AND LOWER(note_properties.value) = :value AND notes.id IS NOT NULL
ORDER BY notes.lft
LIMIT 1
""")
abstract fun firstNoteHavingPropertyLowerCase(name: String, value: String): NoteIdBookId?
abstract fun allNotesHavingPropertyLowerCase(name: String, value: String): List<NoteIdBookId>

@Query("""
UPDATE notes
Expand Down
8 changes: 6 additions & 2 deletions app/src/main/java/com/orgzly/android/di/AppComponent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,10 @@ import com.orgzly.android.reminders.NoteReminders
import com.orgzly.android.reminders.RemindersBroadcastReceiver
import com.orgzly.android.sync.SyncWorker
import com.orgzly.android.ui.BookChooserActivity
import com.orgzly.android.ui.logs.AppLogsActivity
import com.orgzly.android.ui.TemplateChooserActivity
import com.orgzly.android.ui.books.BooksFragment
import com.orgzly.android.ui.logs.AppLogsActivity
import com.orgzly.android.ui.main.MainActivity
import com.orgzly.android.ui.sync.SyncFragment
import com.orgzly.android.ui.note.NoteFragment
import com.orgzly.android.ui.notes.NotesFragment
import com.orgzly.android.ui.notes.book.BookFragment
Expand All @@ -33,7 +32,10 @@ import com.orgzly.android.ui.repos.ReposActivity
import com.orgzly.android.ui.savedsearch.SavedSearchFragment
import com.orgzly.android.ui.savedsearches.SavedSearchesFragment
import com.orgzly.android.ui.settings.SettingsActivity
import com.orgzly.android.ui.settings.exporting.SettingsExportFragment
import com.orgzly.android.ui.settings.importing.SettingsImportFragment
import com.orgzly.android.ui.share.ShareActivity
import com.orgzly.android.ui.sync.SyncFragment
import com.orgzly.android.usecase.UseCaseRunner
import com.orgzly.android.usecase.UseCaseWorker
import com.orgzly.android.widgets.ListWidgetProvider
Expand Down Expand Up @@ -74,6 +76,8 @@ interface AppComponent {
fun inject(arg: SavedSearchesFragment)
fun inject(arg: SavedSearchFragment)
fun inject(arg: RefileFragment)
fun inject(arg: SettingsExportFragment)
fun inject(arg: SettingsImportFragment)
fun inject(arg: SyncFragment)

fun inject(arg: SyncWorker)
Expand Down
25 changes: 25 additions & 0 deletions app/src/main/java/com/orgzly/android/prefs/AppPreferences.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@

import java.io.File;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
Expand Down Expand Up @@ -60,6 +62,11 @@ public static AppPreferencesValues getAllValues(Context context) {
return values;
}

public static void setDefaultPrefsFromJsonMap(Context context, Map<String, ?> parsedMap) {
SharedPreferences prefs = getDefaultSharedPreferences(context);
setPrefsFromValues(prefs, parsedMap);
}

public static void setAllFromValues(Context context, AppPreferencesValues values) {
AppPreferences.clearAllSharedPreferences(context);

Expand Down Expand Up @@ -92,6 +99,10 @@ private static void setPrefsFromValues(SharedPreferences prefs, Map<String, ?> v

} else if (value instanceof Set) {
edit.putStringSet(key, (Set) value);

} else if (value instanceof ArrayList) {
HashSet<String> set = new HashSet<>((Collection<? extends String>) value);
edit.putStringSet(key, set);
}
}

Expand Down Expand Up @@ -1143,6 +1154,20 @@ public static void subfolderSupport(Context context, boolean value) {
getDefaultSharedPreferences(context).edit().putBoolean(key, value).apply();
}

/*
* Export and import of user settings
*/

public static String settingsExportAndImportNoteId(Context context) {
return getDefaultSharedPreferences(context).getString(
context.getResources().getString(R.string.pref_key_note_id_for_settings_export_and_import), "");
}

public static void settingsExportAndImportNoteId(Context context, String value) {
String key = context.getResources().getString(R.string.pref_key_note_id_for_settings_export_and_import);
getDefaultSharedPreferences(context).edit().putString(key, value).apply();
}

/*
* Repository properties map
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,19 @@ class MainActivityViewModel(private val dataRepository: DataRepository) : Common

catchAndPostError {
val result = UseCaseRunner.run(useCase)
val data = result.userData as List<*>

if (result.userData == null) {
if (data.isEmpty()) {
val msg = App.getAppContext().getString(R.string.no_such_link_target, name, value)
errorEvent.postValue(Throwable(msg))

} else {
when (result.userData) {
if (data.size > 1) {
val msg = App.getAppContext().getString(R.string.error_multiple_notes_with_matching_property_value, name, value)
errorEvent.postValue(Throwable(msg))
}
when (data[0]) {
is NoteDao.NoteIdBookId -> {
val noteIdBookId = result.userData
val noteIdBookId = data[0] as NoteDao.NoteIdBookId

when (AppPreferences.linkTarget(App.getAppContext())) {
"note_details" ->
Expand All @@ -104,7 +108,8 @@ class MainActivityViewModel(private val dataRepository: DataRepository) : Common
}
}
is Book -> {
navigationActions.postValue(MainNavigationAction.OpenBook(result.userData.id))
val book = data[0] as Book
navigationActions.postValue(MainNavigationAction.OpenBook(book.id))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import com.orgzly.android.App
import com.orgzly.android.ui.CommonActivity
import com.orgzly.android.ui.settings.SettingsFragment.Listener
import com.orgzly.android.ui.showSnackbar
import com.orgzly.android.usecase.*
import com.orgzly.android.usecase.BookImportGettingStarted
import com.orgzly.android.usecase.DatabaseClear
import com.orgzly.android.usecase.UseCase
import com.orgzly.android.usecase.UseCaseRunner
import com.orgzly.android.usecase.UseCaseWorker
import com.orgzly.databinding.ActivitySettingsBinding

class SettingsActivity : CommonActivity(), Listener {
Expand Down
Loading
Loading