diff --git a/app/src/main/java/com/orgzly/android/data/DataRepository.kt b/app/src/main/java/com/orgzly/android/data/DataRepository.kt index 34a485dcd..792e85239 100644 --- a/app/src/main/java/com/orgzly/android/data/DataRepository.kt +++ b/app/src/main/java/com/orgzly/android/data/DataRepository.kt @@ -599,7 +599,7 @@ class DataRepository @Inject constructor( } else { db.runInTransaction(Callable { - moveSubtrees(noteIds, Place.UNDER, target.noteId) + moveSubtrees(noteIds, target.place, target.noteId) }) } } @@ -1239,12 +1239,14 @@ class DataRepository @Inject constructor( } fun getNoteAtPath(bookName: String, path: String) = - getNotes(bookName) - .filter { ("/$path").endsWith("/" + it.note.title) } - .firstOrNull { view -> - getNoteAndAncestors(view.note.id) - .joinToString("/") { it.title } == path - } + if (path.split("/").any { it.isNotEmpty() }) + getNotes(bookName) + .filter { ("/$path").endsWith("/" + it.note.title) } + .firstOrNull { view -> + getNoteAndAncestors(view.note.id) + .joinToString("/") { it.title } == path + } + else null fun getNotesAndSubtrees(ids: Set): List { return db.note().getNotesForSubtrees(ids) diff --git a/app/src/main/java/com/orgzly/android/external/ExternalAccessReceiver.kt b/app/src/main/java/com/orgzly/android/external/ExternalAccessReceiver.kt index d31735b6b..97b8ec116 100644 --- a/app/src/main/java/com/orgzly/android/external/ExternalAccessReceiver.kt +++ b/app/src/main/java/com/orgzly/android/external/ExternalAccessReceiver.kt @@ -12,6 +12,7 @@ class ExternalAccessReceiver : BroadcastReceiver() { GetOrgInfo(), RunSearch(), EditNotes(), + EditSavedSearches(), ManageWidgets() ) diff --git a/app/src/main/java/com/orgzly/android/external/actionhandlers/EditNotes.kt b/app/src/main/java/com/orgzly/android/external/actionhandlers/EditNotes.kt index 65fe506ed..3dbb002d5 100644 --- a/app/src/main/java/com/orgzly/android/external/actionhandlers/EditNotes.kt +++ b/app/src/main/java/com/orgzly/android/external/actionhandlers/EditNotes.kt @@ -1,85 +1,63 @@ package com.orgzly.android.external.actionhandlers -import android.content.Context import android.content.Intent -import com.google.gson.JsonElement -import com.google.gson.JsonObject -import com.google.gson.JsonParseException -import com.google.gson.JsonParser import com.orgzly.android.external.types.Response -import com.orgzly.android.ui.NotePlace -import com.orgzly.android.ui.Place -import com.orgzly.android.ui.note.NotePayload -import com.orgzly.org.OrgProperties class EditNotes : ExternalAccessActionHandler() { override val actions = listOf( - action(::addNote, "ADD_NOTE", "ADD_NOTES"), - action(::deleteNote, "DELETE_NOTE", "DELETE_NOTES") + action(::addNote, "ADD_NOTE", "ADD_NOTES"), + action(::editNote, "EDIT_NOTE"), + action(::refileNote, "REFILE_NOTE", "REFILE_NOTES"), + action(::moveNote, "MOVE_NOTE", "MOVE_NOTES"), + action(::deleteNote, "DELETE_NOTE", "DELETE_NOTES") ) - fun addNote(intent: Intent): Response { - val book = getBook(intent) ?: return Response(false, "Couldn't find specified book") - val newNote = notePayloadFromJson(intent.getStringExtra("PAYLOAD") ?: "") + private fun addNote(intent: Intent): Response { + val place = intent.getNotePlace() + ?: return Response(false, "Could not find parent note") + val newNote = intent.getNotePayload() ?: return Response(false, "Invalid payload") - val path = intent.getStringExtra("PATH") ?: "" - - val place = if (path.split("/").any { it.isNotEmpty() }) { - dataRepository.getNoteAtPath(book.name, path)?.let { - NotePlace(book.id, it.note.id, Place.UNDER) - } - } else null - place ?: return Response(false, "Couldn't find parent note at path") - dataRepository.createNote(newNote, place) - return Response(true, null) + val note = dataRepository.createNote(newNote, place) + return Response(true, "${note.id}") } - // - - private fun JsonObject.getString(name: String) = this[name]?.let { - if (it.isJsonPrimitive && it.asJsonPrimitive.isString) - it.asJsonPrimitive.asString - else null + private fun editNote(intent: Intent): Response { + val noteView = intent.getNote() + ?: return Response(false, "Couldn't find note") + val newNote = intent.getNotePayload(title=noteView.note.title) + ?: return Response(false, "Invalid payload") + dataRepository.updateNote(noteView.note.id, newNote) + return Response() } - private val JsonElement.asMap: Map? - get() = if (this.isJsonObject) { - this.asJsonObject - .entrySet() - .map { - if (it.value.isJsonPrimitive) - it.key to it.value.asJsonPrimitive.asString - else return null - } - .toMap() - } else null + private fun refileNote(intent: Intent): Response { + val notes = intent.getNoteIds() + if (notes.isEmpty()) + return Response(false, "No notes specified") + val place = intent.getNotePlace() + ?: return Response(false, "Couldn't find note") + dataRepository.refileNotes(notes, place) + return Response() + } - private fun notePayloadFromJson(rawJson: String): NotePayload? { - val json = try { - JsonParser.parseString(rawJson) - .let { if (it.isJsonObject) it.asJsonObject else null } - } catch (e: JsonParseException) { - null - } - return try { - json!! - NotePayload( - json.getString("title")!!, - json.getString("content"), - json.getString("state"), - json.getString("priority"), - json.getString("scheduled"), - json.getString("deadline"), - json.getString("closed"), - (json.getString("tags") ?: "") - .split(" +".toRegex()) - .filter { it.isNotEmpty() }, - OrgProperties().apply { - json["properties"]?.asMap?.forEach { (k, v) -> this[k] = v } - } - ) - } catch (e: NullPointerException) { null } + private fun moveNote(intent: Intent): Response { + val notes = intent.getNoteIds() + if (notes.isEmpty()) return Response(false, "No notes specified") + with(dataRepository) { when (intent.getStringExtra("DIRECTION")) { + "UP" -> intent.getBook()?.id?.let { moveNote(it, notes, -1) } + "DOWN" -> intent.getBook()?.id?.let { moveNote(it, notes, 1) } + "LEFT" -> promoteNotes(notes) + "RIGHT" -> demoteNotes(notes) + else -> return Response(false, "Invalid direction") + } } + return Response() } - // + private fun deleteNote(intent: Intent): Response { + val book = intent.getBook() ?: return Response(false, "Couldn't find specified book") + val notes = intent.getNoteIds() + if (notes.isEmpty()) return Response(false, "No notes specified") + dataRepository.deleteNotes(book.id, notes) + return Response() + } } \ No newline at end of file diff --git a/app/src/main/java/com/orgzly/android/external/actionhandlers/EditSavedSearches.kt b/app/src/main/java/com/orgzly/android/external/actionhandlers/EditSavedSearches.kt new file mode 100644 index 000000000..6df8c05d9 --- /dev/null +++ b/app/src/main/java/com/orgzly/android/external/actionhandlers/EditSavedSearches.kt @@ -0,0 +1,48 @@ +package com.orgzly.android.external.actionhandlers + +import android.content.Intent +import com.orgzly.android.db.entity.SavedSearch +import com.orgzly.android.external.types.Response + +class EditSavedSearches : ExternalAccessActionHandler() { + override val actions = listOf( + action(::addSavedSearch, "ADD_SAVED_SEARCH"), + action(::editSavedSearch, "EDIT_SAVED_SEARCH"), + action(::moveSavedSearch, "MOVE_SAVED_SEARCH"), + action(::deleteSavedSearch, "DELETE_SAVED_SEARCH"), + ) + + private fun addSavedSearch(intent: Intent) = + intent.getNewSavedSearch()?.let { + val id = dataRepository.createSavedSearch(it) + Response(true, "$id") + } ?: Response(false, "Invalid saved search details") + + private fun editSavedSearch(intent: Intent) = intent.getSavedSearch()?.let { savedSearch -> + intent.getNewSavedSearch(allowBlank = true)?.let { newSavedSearch -> + dataRepository.updateSavedSearch(SavedSearch( + savedSearch.id, + (if (newSavedSearch.name.isBlank()) savedSearch.name + else newSavedSearch.name), + (if (newSavedSearch.query.isBlank()) savedSearch.query + else newSavedSearch.query), + savedSearch.position + )) + return Response() + } ?: Response(false, "Invalid saved search details") + } ?: Response(false, "Couldn't find saved search") + + private fun moveSavedSearch(intent: Intent) = intent.getSavedSearch()?.let { savedSearch -> + when (intent.getStringExtra("DIRECTION")) { + "UP" -> dataRepository.moveSavedSearchUp(savedSearch.id) + "DOWN" -> dataRepository.moveSavedSearchDown(savedSearch.id) + else -> return Response(false, "Invalid direction") + } + return Response() + } ?: Response(false, "Couldn't find saved search") + + private fun deleteSavedSearch(intent: Intent) = intent.getSavedSearch()?.let { savedSearch -> + dataRepository.deleteSavedSearches(setOf(savedSearch.id)) + return Response() + } ?: Response(false, "Couldn't find saved search") +} \ No newline at end of file diff --git a/app/src/main/java/com/orgzly/android/external/actionhandlers/ExternalAccessActionHandler.kt b/app/src/main/java/com/orgzly/android/external/actionhandlers/ExternalAccessActionHandler.kt index 7206894b0..c5f4b4fa3 100644 --- a/app/src/main/java/com/orgzly/android/external/actionhandlers/ExternalAccessActionHandler.kt +++ b/app/src/main/java/com/orgzly/android/external/actionhandlers/ExternalAccessActionHandler.kt @@ -2,10 +2,19 @@ package com.orgzly.android.external.actionhandlers import android.content.Context import android.content.Intent +import com.google.gson.JsonElement +import com.google.gson.JsonObject +import com.google.gson.JsonParseException +import com.google.gson.JsonParser import com.orgzly.android.App import com.orgzly.android.data.DataRepository import com.orgzly.android.db.entity.Book +import com.orgzly.android.db.entity.SavedSearch import com.orgzly.android.external.types.Response +import com.orgzly.android.ui.NotePlace +import com.orgzly.android.ui.Place +import com.orgzly.android.ui.note.NotePayload +import com.orgzly.org.OrgProperties import javax.inject.Inject abstract class ExternalAccessActionHandler { @@ -21,9 +30,100 @@ abstract class ExternalAccessActionHandler { actions.flatten().toMap().mapKeys { (key, _) -> "com.orgzly.android.$key" } } - fun getBook(intent: Intent) = - dataRepository.getBook(intent.getLongExtra("BOOK_ID", -1)) - ?: dataRepository.getBook(intent.getStringExtra("BOOK_NAME") ?: "") + fun Intent.getNotePayload(title: String? = null): NotePayload? { + val rawJson = getStringExtra("NOTE_PAYLOAD") + val json = try { + JsonParser.parseString(rawJson) + .let { if (it.isJsonObject) it.asJsonObject else null } + } catch (e: JsonParseException) { + null + } + + return try { + json!! + NotePayload( + (json.getString("title") ?: title)!!, + json.getString("content"), + json.getString("state"), + json.getString("priority"), + json.getString("scheduled"), + json.getString("deadline"), + json.getString("closed"), + (json.getString("tags") ?: "") + .split(" +".toRegex()) + .filter { it.isNotEmpty() }, + OrgProperties().apply { + json["properties"]?.asMap?.forEach { (k, v) -> this[k] = v } + } + ) + } catch (e: NullPointerException) { null } + } + + fun Intent.getNotePlace(book_: Book? = null) = (book_ ?: getBook(prefix="PARENT_")) + ?.let { book -> getNote(book, prefix="PARENT_")?.let { noteView -> + val place = try { + Place.valueOf(getStringExtra("PLACEMENT") ?: "") + } catch (e: IllegalArgumentException) { Place.UNDER } + NotePlace(book.id, noteView.note.id, place) + } } + + fun Intent.getNoteIds(allowSingle: Boolean = true): Set { + val id = if (allowSingle) getLongExtra("NOTE_ID", -1) else null + val ids = getLongArrayExtra("NOTE_IDS")?.toTypedArray() ?: emptyArray() + val book = getBook()?.name + val (path, paths) = if (book != null) { + val path = + if (allowSingle) + getStringExtra("NOTE_PATH") + ?.let { dataRepository.getNoteAtPath(book, it)?.note?.id } + else null + val paths = (getStringArrayExtra("NOTE_PATHS") ?: emptyArray()) + .mapNotNull { dataRepository.getNoteAtPath(book, it)?.note?.id } + .toTypedArray() + path to paths + } else null to emptyArray() + return listOfNotNull(id, *ids, path, *paths).filter { it >= 0 }.toSet() + } + + fun Intent.getNote(book: Book? = null, prefix: String = "") = + dataRepository.getNoteView(getLongExtra("${prefix}NOTE_ID", -1)) + ?: (book ?: getBook(prefix=prefix)) + ?.let { dataRepository.getNoteAtPath(it.name, + getStringExtra("${prefix}NOTE_PATH") ?: "") } + + fun Intent.getBook(prefix: String = "") = + dataRepository.getBook(getLongExtra("${prefix}BOOK_ID", -1)) + ?: dataRepository.getBook(getStringExtra("${prefix}BOOK_NAME") ?: "") + + fun Intent.getSavedSearch() = + dataRepository.getSavedSearch(getLongExtra("SAVED_SEARCH_ID", -1)) + ?: dataRepository.getSavedSearches() + .find { it.name == getStringExtra("SAVED_SEARCH_NAME") } + + fun Intent.getNewSavedSearch(allowBlank: Boolean = false): SavedSearch? { + val name = getStringExtra("SAVED_SEARCH_NEW_NAME") + val query = getStringExtra("SAVED_SEARCH_NEW_QUERY") + if (!allowBlank && (name.isNullOrBlank() || query.isNullOrBlank())) return null + return SavedSearch(0, name ?: "", query ?: "", 0) + } + + private fun JsonObject.getString(name: String) = this[name]?.let { + if (it.isJsonPrimitive && it.asJsonPrimitive.isString) + it.asJsonPrimitive.asString + else null + } + + private val JsonElement.asMap: Map? + get() = if (this.isJsonObject) { + this.asJsonObject + .entrySet() + .map { + if (it.value.isJsonPrimitive) + it.key to it.value.asJsonPrimitive.asString + else return null + } + .toMap() + } else null fun action(f: (Intent, Context) -> Response, vararg names: String) = names.map { it to f } diff --git a/app/src/main/java/com/orgzly/android/external/actionhandlers/GetOrgInfo.kt b/app/src/main/java/com/orgzly/android/external/actionhandlers/GetOrgInfo.kt index ab51f8895..8a3406fa8 100644 --- a/app/src/main/java/com/orgzly/android/external/actionhandlers/GetOrgInfo.kt +++ b/app/src/main/java/com/orgzly/android/external/actionhandlers/GetOrgInfo.kt @@ -1,6 +1,5 @@ package com.orgzly.android.external.actionhandlers -import android.content.Context import android.content.Intent import com.orgzly.android.external.types.* @@ -23,12 +22,8 @@ class GetOrgInfo : ExternalAccessActionHandler() { .map(SavedSearch::from).toTypedArray() ) - private fun getNote(intent: Intent): Response { - val book = getBook(intent) ?: return Response(false, "Couldn't find specified book") - val path = intent.getStringExtra("PATH") - ?: return Response(false, "Invalid arguments!") - return dataRepository.getNoteAtPath(book.name, path) + private fun getNote(intent: Intent) = + intent.getNote() ?.let { Response(true, Note.from(it)) } ?: Response(false, "Couldn't find note at specified path!") - } } \ No newline at end of file diff --git a/app/src/main/java/com/orgzly/android/external/actionhandlers/ManageWidgets.kt b/app/src/main/java/com/orgzly/android/external/actionhandlers/ManageWidgets.kt index ede400f3b..7b34eb0ad 100644 --- a/app/src/main/java/com/orgzly/android/external/actionhandlers/ManageWidgets.kt +++ b/app/src/main/java/com/orgzly/android/external/actionhandlers/ManageWidgets.kt @@ -26,15 +26,15 @@ class ManageWidgets : ExternalAccessActionHandler() { private fun setWidget(intent: Intent, context: Context): Response { val widgetId = intent.getIntExtra("WIDGET_ID", -1) if (widgetId < 0) return Response(false, "invalid widget ID") - val savedSearchId = intent.getLongExtra("SAVED_SEARCH_ID", -1) - if (savedSearchId < 0) return Response(false, "invalid saved search ID") + val savedSearch = intent.getSavedSearch() + ?: return Response(false, "invalid saved search ID") context.sendBroadcast(Intent(context, ListWidgetProvider::class.java).apply { action = AppIntent.ACTION_SET_LIST_WIDGET_SELECTION putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId) - putExtra(AppIntent.EXTRA_SAVED_SEARCH_ID, savedSearchId) + putExtra(AppIntent.EXTRA_SAVED_SEARCH_ID, savedSearch.id) }) - return Response(true, null) + return Response() } } diff --git a/app/src/main/java/com/orgzly/android/external/types/Note.kt b/app/src/main/java/com/orgzly/android/external/types/Note.kt index cc4396f17..eedba1876 100644 --- a/app/src/main/java/com/orgzly/android/external/types/Note.kt +++ b/app/src/main/java/com/orgzly/android/external/types/Note.kt @@ -3,6 +3,7 @@ package com.orgzly.android.external.types import com.orgzly.android.db.entity.NoteView data class Note( + val id: Long, val title: String, val content: String?, val tags: List, @@ -20,6 +21,7 @@ data class Note( fun from(view: NoteView): Note { val note = view.note return Note( + note.id, note.title, note.content, note.tags?.split(" +".toRegex()) diff --git a/app/src/main/java/com/orgzly/android/external/types/Response.kt b/app/src/main/java/com/orgzly/android/external/types/Response.kt index 7600e7c1b..41bf95860 100644 --- a/app/src/main/java/com/orgzly/android/external/types/Response.kt +++ b/app/src/main/java/com/orgzly/android/external/types/Response.kt @@ -2,7 +2,7 @@ package com.orgzly.android.external.types import java.io.Serializable -data class Response(val success: Boolean, val result: Any?) { +data class Response(val success: Boolean = true, val result: Any? = null) { constructor(success: Boolean, result: List) : this(success, result.toTypedArray()) } \ No newline at end of file