From a21dc741e27873c76dae061e7c6d4cf13ee6ce45 Mon Sep 17 00:00:00 2001 From: Nat Karmios Date: Mon, 16 Aug 2021 23:45:25 +0100 Subject: [PATCH 01/12] Add external-access receiver --- app/src/main/AndroidManifest.xml | 4 ++++ .../external/ExternalAccessReceiver.kt | 21 +++++++++++++++++++ .../ExternalAccessActionHandler.kt | 15 +++++++++++++ .../orgzly/android/external/types/Response.kt | 8 +++++++ 4 files changed, 48 insertions(+) create mode 100644 app/src/main/java/com/orgzly/android/external/ExternalAccessReceiver.kt create mode 100644 app/src/main/java/com/orgzly/android/external/actionhandlers/ExternalAccessActionHandler.kt create mode 100644 app/src/main/java/com/orgzly/android/external/types/Response.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f8c11f441..3538868ce 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -221,6 +221,10 @@ + + + diff --git a/app/src/main/java/com/orgzly/android/external/ExternalAccessReceiver.kt b/app/src/main/java/com/orgzly/android/external/ExternalAccessReceiver.kt new file mode 100644 index 000000000..662ff19b6 --- /dev/null +++ b/app/src/main/java/com/orgzly/android/external/ExternalAccessReceiver.kt @@ -0,0 +1,21 @@ +package com.orgzly.android.external + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import com.google.gson.GsonBuilder +import com.orgzly.android.external.actionhandlers.* +import com.orgzly.android.external.types.Response + +class ExternalAccessReceiver : BroadcastReceiver() { + val actionHandlers = emptyList() + + override fun onReceive(context: Context?, intent: Intent?) { + val response = actionHandlers.asSequence() + .mapNotNull { it.handle(intent!!, context!!) } + .firstOrNull() + ?: Response(false, "Invalid action") + val gson = GsonBuilder().serializeNulls().create() + resultData = gson.toJson(response) + } +} 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 new file mode 100644 index 000000000..1d1bae8b0 --- /dev/null +++ b/app/src/main/java/com/orgzly/android/external/actionhandlers/ExternalAccessActionHandler.kt @@ -0,0 +1,15 @@ +package com.orgzly.android.external.actionhandlers + +import android.content.Context +import android.content.Intent +import com.orgzly.android.external.types.Response + +abstract class ExternalAccessActionHandler { + abstract val actions: Map Response> + private val fullNameActions by lazy { + actions.mapKeys { (key, _) -> "com.orgzly.android.$key" } + } + + fun handle(intent: Intent, context: Context) = + fullNameActions[intent.action!!]?.let { it(intent, context) } +} \ No newline at end of file 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 new file mode 100644 index 000000000..7600e7c1b --- /dev/null +++ b/app/src/main/java/com/orgzly/android/external/types/Response.kt @@ -0,0 +1,8 @@ +package com.orgzly.android.external.types + +import java.io.Serializable + +data class Response(val success: Boolean, val result: Any?) { + constructor(success: Boolean, result: List) : + this(success, result.toTypedArray()) +} \ No newline at end of file From 1aa1c8ebfd1c4d6a0a1ea6d343d43da6ca619182 Mon Sep 17 00:00:00 2001 From: Nat Karmios Date: Mon, 16 Aug 2021 23:48:38 +0100 Subject: [PATCH 02/12] Add some helper functions for external-access receiver --- .../main/java/com/orgzly/android/data/DataRepository.kt | 8 ++++++++ .../com/orgzly/android/widgets/ListWidgetProvider.java | 6 +++++- 2 files changed, 13 insertions(+), 1 deletion(-) 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 d27570804..f91234b20 100644 --- a/app/src/main/java/com/orgzly/android/data/DataRepository.kt +++ b/app/src/main/java/com/orgzly/android/data/DataRepository.kt @@ -1280,6 +1280,14 @@ class DataRepository @Inject constructor( return db.note().getNoteAndAncestors(noteId) } + fun getNoteAtPath(bookName: String, path: String) = + getNotes(bookName) + .filter { ("/$path").endsWith("/" + it.note.title) } + .firstOrNull { view -> + getNoteAndAncestors(view.note.id) + .joinToString("/") { it.title } == path + } + fun getNotesAndSubtrees(ids: Set): List { return db.note().getNotesForSubtrees(ids) } diff --git a/app/src/main/java/com/orgzly/android/widgets/ListWidgetProvider.java b/app/src/main/java/com/orgzly/android/widgets/ListWidgetProvider.java index fd42a9d60..b541eedc1 100644 --- a/app/src/main/java/com/orgzly/android/widgets/ListWidgetProvider.java +++ b/app/src/main/java/com/orgzly/android/widgets/ListWidgetProvider.java @@ -242,6 +242,10 @@ private void setSelectionFromIntent(Context context, Intent intent) { } private SavedSearch getSavedSearch(Context context, int appWidgetId) { + return getSavedSearch(context, appWidgetId, dataRepository); + } + + public static SavedSearch getSavedSearch(Context context, int appWidgetId, DataRepository dataRepository) { long filterId = context.getSharedPreferences(PREFERENCES_ID, Context.MODE_PRIVATE) .getLong(getFilterPreferenceKey(appWidgetId), -1); @@ -265,7 +269,7 @@ private void setFilter(Context context, int appWidgetId, long id) { editor.apply(); } - private String getFilterPreferenceKey(int appWidgetId) { + private static String getFilterPreferenceKey(int appWidgetId) { return "widget-filter-" + appWidgetId; } From 3b6a4354e02d682cd5f9be6f42dbd5ffe2a94c43 Mon Sep 17 00:00:00 2001 From: Nat Karmios Date: Mon, 16 Aug 2021 23:51:09 +0100 Subject: [PATCH 03/12] Add response data classes for external-access receiver --- .../com/orgzly/android/external/types/Book.kt | 10 +++ .../com/orgzly/android/external/types/Note.kt | 75 +++++++++++++++++++ .../android/external/types/SavedSearch.kt | 8 ++ 3 files changed, 93 insertions(+) create mode 100644 app/src/main/java/com/orgzly/android/external/types/Book.kt create mode 100644 app/src/main/java/com/orgzly/android/external/types/Note.kt create mode 100644 app/src/main/java/com/orgzly/android/external/types/SavedSearch.kt diff --git a/app/src/main/java/com/orgzly/android/external/types/Book.kt b/app/src/main/java/com/orgzly/android/external/types/Book.kt new file mode 100644 index 000000000..31b624aaf --- /dev/null +++ b/app/src/main/java/com/orgzly/android/external/types/Book.kt @@ -0,0 +1,10 @@ +package com.orgzly.android.external.types + +import com.orgzly.android.db.entity.BookView + +data class Book(val id: Long, val title: String) { + companion object { + fun from(view: BookView) = + Book(view.book.id, view.book.name) + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..cc4396f17 --- /dev/null +++ b/app/src/main/java/com/orgzly/android/external/types/Note.kt @@ -0,0 +1,75 @@ +package com.orgzly.android.external.types + +import com.orgzly.android.db.entity.NoteView + +data class Note( + val title: String, + val content: String?, + val tags: List, + val inheritedTags: List, + val bookName: String, + val scheduled: Timestamp?, + val deadline: Timestamp?, + val closed: Timestamp?, + val priority: String?, + val state: String?, + val createdAt: Long? + +) { + companion object { + fun from(view: NoteView): Note { + val note = view.note + return Note( + note.title, + note.content, + note.tags?.split(" +".toRegex()) + ?.filter { it.isNotEmpty() } + ?: emptyList(), + view.getInheritedTagsList() + .filter { it.isNotEmpty() }, + view.bookName, + Timestamp.from( + view.scheduledTimeTimestamp, + view.scheduledTimeString, + view.scheduledTimeEndString, + view.scheduledRangeString + ), + Timestamp.from( + view.deadlineTimeTimestamp, + view.deadlineTimeString, + view.deadlineTimeEndString, + view.deadlineRangeString + ), + Timestamp.from( + view.closedTimeTimestamp, + view.closedTimeString, + view.closedTimeEndString, + view.closedRangeString + ), + note.priority, + note.state, + note.createdAt + ) + } + } + + data class Timestamp( + val timeTimestamp: Long, + val timeString: String, + val timeEndString: String? = null, + val rangeString: String?, + ) { + companion object { + fun from( + timeTimestamp: Long?, + timeString: String?, + timeEndString: String?, + rangeString: String? + ): Timestamp? { + return if (timeTimestamp != null && timeString != null) + Timestamp(timeTimestamp, timeString, timeEndString, rangeString) + else null + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/orgzly/android/external/types/SavedSearch.kt b/app/src/main/java/com/orgzly/android/external/types/SavedSearch.kt new file mode 100644 index 000000000..4200e3e5b --- /dev/null +++ b/app/src/main/java/com/orgzly/android/external/types/SavedSearch.kt @@ -0,0 +1,8 @@ +package com.orgzly.android.external.types + +data class SavedSearch(val id: Long, val name: String, val position: Int, val query: String) { + companion object { + fun from(search: com.orgzly.android.db.entity.SavedSearch) = + SavedSearch(search.id, search.name, search.position, search.query) + } +} \ No newline at end of file From 7db14b1cd83d5a61b6f8583901cef93db4129d2f Mon Sep 17 00:00:00 2001 From: Nat Karmios Date: Mon, 16 Aug 2021 23:52:09 +0100 Subject: [PATCH 04/12] Add some handlers to external-access receiver --- .../com/orgzly/android/di/AppComponent.kt | 2 + .../external/ExternalAccessReceiver.kt | 7 +- .../external/actionhandlers/EditNotes.kt | 83 +++++++++++++++++++ .../ExternalAccessActionHandler.kt | 10 +++ .../external/actionhandlers/GetOrgInfo.kt | 40 +++++++++ .../external/actionhandlers/ManageWidgets.kt | 41 +++++++++ .../external/actionhandlers/RunSearch.kt | 21 +++++ 7 files changed, 203 insertions(+), 1 deletion(-) create mode 100644 app/src/main/java/com/orgzly/android/external/actionhandlers/EditNotes.kt create mode 100644 app/src/main/java/com/orgzly/android/external/actionhandlers/GetOrgInfo.kt create mode 100644 app/src/main/java/com/orgzly/android/external/actionhandlers/ManageWidgets.kt create mode 100644 app/src/main/java/com/orgzly/android/external/actionhandlers/RunSearch.kt diff --git a/app/src/main/java/com/orgzly/android/di/AppComponent.kt b/app/src/main/java/com/orgzly/android/di/AppComponent.kt index f5b3efb86..d89a48fa4 100644 --- a/app/src/main/java/com/orgzly/android/di/AppComponent.kt +++ b/app/src/main/java/com/orgzly/android/di/AppComponent.kt @@ -7,6 +7,7 @@ import com.orgzly.android.TimeChangeBroadcastReceiver import com.orgzly.android.di.module.ApplicationModule import com.orgzly.android.di.module.DataModule import com.orgzly.android.di.module.DatabaseModule +import com.orgzly.android.external.actionhandlers.ExternalAccessActionHandler import com.orgzly.android.reminders.NoteReminders import com.orgzly.android.reminders.RemindersBroadcastReceiver import com.orgzly.android.sync.SyncWorker @@ -86,4 +87,5 @@ interface AppComponent { fun inject(arg: RemindersBroadcastReceiver) fun inject(arg: NotificationBroadcastReceiver) fun inject(arg: SharingShortcutsManager) + fun inject(arg: ExternalAccessActionHandler) } \ No newline at end of file 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 662ff19b6..d31735b6b 100644 --- a/app/src/main/java/com/orgzly/android/external/ExternalAccessReceiver.kt +++ b/app/src/main/java/com/orgzly/android/external/ExternalAccessReceiver.kt @@ -8,7 +8,12 @@ import com.orgzly.android.external.actionhandlers.* import com.orgzly.android.external.types.Response class ExternalAccessReceiver : BroadcastReceiver() { - val actionHandlers = emptyList() + val actionHandlers = listOf( + GetOrgInfo(), + RunSearch(), + EditNotes(), + ManageWidgets() + ) override fun onReceive(context: Context?, intent: Intent?) { val response = actionHandlers.asSequence() 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 new file mode 100644 index 000000000..31d78caaf --- /dev/null +++ b/app/src/main/java/com/orgzly/android/external/actionhandlers/EditNotes.kt @@ -0,0 +1,83 @@ +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 = mapOf( + "ADD_NOTES" to ::addNote + ) + + @Suppress("UNUSED_PARAMETER") + fun addNote(intent: Intent, context: Context): Response { + val newNote = notePayloadFromJson(intent.getStringExtra("PAYLOAD") ?: "") + ?: return Response(false, "Invalid payload") + val path = intent.getStringExtra("PATH") ?: "" + val bookName = intent.getStringExtra("BOOK") + val bookId = dataRepository.getBooks().find { it.book.name == bookName }?.book?.id + ?: return Response(false, "Couldn't find specified book") + + val place = if (path.split("/").any { it.isNotEmpty() }) { + dataRepository.getNoteAtPath(bookName!!, path)?.let { + NotePlace(bookId, 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) + } + + 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 + + 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 } + } +} \ 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 1d1bae8b0..aecf01ca8 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,9 +2,19 @@ package com.orgzly.android.external.actionhandlers import android.content.Context import android.content.Intent +import com.orgzly.android.App +import com.orgzly.android.data.DataRepository import com.orgzly.android.external.types.Response +import javax.inject.Inject abstract class ExternalAccessActionHandler { + @Inject + lateinit var dataRepository: DataRepository + + init { + App.appComponent.inject(this) + } + abstract val actions: Map Response> private val fullNameActions by lazy { actions.mapKeys { (key, _) -> "com.orgzly.android.$key" } 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 new file mode 100644 index 000000000..765936b14 --- /dev/null +++ b/app/src/main/java/com/orgzly/android/external/actionhandlers/GetOrgInfo.kt @@ -0,0 +1,40 @@ +package com.orgzly.android.external.actionhandlers + +import android.content.Context +import android.content.Intent +import com.orgzly.android.external.types.* + +class GetOrgInfo : ExternalAccessActionHandler() { + override val actions = mapOf( + "GET_BOOKS" to ::getBooks, + "GET_SAVED_SEARCHES" to ::getSavedSearches, + "GET_NOTE" to ::getNote + ) + + @Suppress("UNUSED_PARAMETER") + private fun getBooks(intent: Intent, context: Context) = Response( + true, + dataRepository.getBooks() + .map(Book::from).toTypedArray() + ) + + @Suppress("UNUSED_PARAMETER") + private fun getSavedSearches(intent: Intent, context: Context) = Response( + true, + dataRepository.getSavedSearches() + .map(SavedSearch::from).toTypedArray() + ) + + @Suppress("UNUSED_PARAMETER") + private fun getNote(intent: Intent, context: Context): Response { + val bookName = intent.getStringExtra("BOOK_NAME") + val path = intent.getStringExtra("PATH") + return if (bookName == null || path == null) { + Response(false, "Invalid arguments!") + } else { + dataRepository.getNoteAtPath(bookName, path) + ?.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 new file mode 100644 index 000000000..0e805acdd --- /dev/null +++ b/app/src/main/java/com/orgzly/android/external/actionhandlers/ManageWidgets.kt @@ -0,0 +1,41 @@ +package com.orgzly.android.external.actionhandlers + +import android.appwidget.AppWidgetManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import com.orgzly.android.AppIntent +import com.orgzly.android.external.types.Response +import com.orgzly.android.widgets.ListWidgetProvider + +class ManageWidgets : ExternalAccessActionHandler() { + override val actions = mapOf( + "GET_WIDGETS" to ::getWidgets, + "SET_WIDGET" to ::setWidget + ) + + @Suppress("UNUSED_PARAMETER") + private fun getWidgets(intent: Intent, context: Context): Response { + val widgetManager = AppWidgetManager.getInstance(context) + val componentName = ComponentName(context.packageName, ListWidgetProvider::class.java.name) + val widgetData = widgetManager.getAppWidgetIds(componentName) + .map { it to ListWidgetProvider.getSavedSearch(context, it, dataRepository) } + .toMap() + return Response(true, widgetData) + } + + 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") + + 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) + }) + + return Response(true, null) + } +} diff --git a/app/src/main/java/com/orgzly/android/external/actionhandlers/RunSearch.kt b/app/src/main/java/com/orgzly/android/external/actionhandlers/RunSearch.kt new file mode 100644 index 000000000..10d505dc2 --- /dev/null +++ b/app/src/main/java/com/orgzly/android/external/actionhandlers/RunSearch.kt @@ -0,0 +1,21 @@ +package com.orgzly.android.external.actionhandlers + +import android.content.Context +import android.content.Intent +import com.orgzly.android.query.user.InternalQueryParser +import com.orgzly.android.external.types.* + +class RunSearch : ExternalAccessActionHandler() { + override val actions = mapOf( + "SEARCH" to ::runSearch + ) + + @Suppress("UNUSED_PARAMETER") + private fun runSearch(intent: Intent, context: Context): Response { + val searchTerm = intent.getStringExtra("QUERY") + if (searchTerm.isNullOrBlank()) return Response(false, "Invalid search term!") + val query = InternalQueryParser().parse(searchTerm) + val notes = dataRepository.selectNotesFromQuery(query) + return Response(true, notes.map(Note::from).toTypedArray()) + } +} \ No newline at end of file From 6c6ee3dbb4f607a91a24df47e5eacc1274b55ce3 Mon Sep 17 00:00:00 2001 From: Nat Karmios Date: Tue, 17 Aug 2021 19:50:18 +0100 Subject: [PATCH 05/12] Add getBook() helper --- .../android/external/actionhandlers/EditNotes.kt | 8 +++----- .../actionhandlers/ExternalAccessActionHandler.kt | 5 +++++ .../android/external/actionhandlers/GetOrgInfo.kt | 13 +++++-------- 3 files changed, 13 insertions(+), 13 deletions(-) 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 31d78caaf..754c28715 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 @@ -19,16 +19,14 @@ class EditNotes : ExternalAccessActionHandler() { @Suppress("UNUSED_PARAMETER") fun addNote(intent: Intent, context: Context): Response { + val book = getBook(intent) ?: return Response(false, "Couldn't find specified book") val newNote = notePayloadFromJson(intent.getStringExtra("PAYLOAD") ?: "") ?: return Response(false, "Invalid payload") val path = intent.getStringExtra("PATH") ?: "" - val bookName = intent.getStringExtra("BOOK") - val bookId = dataRepository.getBooks().find { it.book.name == bookName }?.book?.id - ?: return Response(false, "Couldn't find specified book") val place = if (path.split("/").any { it.isNotEmpty() }) { - dataRepository.getNoteAtPath(bookName!!, path)?.let { - NotePlace(bookId, it.note.id, Place.UNDER) + 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") 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 aecf01ca8..ed411b874 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 @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import com.orgzly.android.App import com.orgzly.android.data.DataRepository +import com.orgzly.android.db.entity.Book import com.orgzly.android.external.types.Response import javax.inject.Inject @@ -20,6 +21,10 @@ abstract class ExternalAccessActionHandler { actions.mapKeys { (key, _) -> "com.orgzly.android.$key" } } + fun getBook(intent: Intent) = + dataRepository.getBook(intent.getLongExtra("BOOK_ID", -1)) + ?: dataRepository.getBook(intent.getStringExtra("BOOK_NAME") ?: "") + fun handle(intent: Intent, context: Context) = fullNameActions[intent.action!!]?.let { it(intent, context) } } \ No newline at end of file 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 765936b14..e9e331b1b 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 @@ -27,14 +27,11 @@ class GetOrgInfo : ExternalAccessActionHandler() { @Suppress("UNUSED_PARAMETER") private fun getNote(intent: Intent, context: Context): Response { - val bookName = intent.getStringExtra("BOOK_NAME") + val book = getBook(intent) ?: return Response(false, "Couldn't find specified book") val path = intent.getStringExtra("PATH") - return if (bookName == null || path == null) { - Response(false, "Invalid arguments!") - } else { - dataRepository.getNoteAtPath(bookName, path) - ?.let { Response(true, Note.from(it)) } - ?: Response(false, "Couldn't find note at specified path!") - } + ?: return Response(false, "Invalid arguments!") + return dataRepository.getNoteAtPath(book.name, path) + ?.let { Response(true, Note.from(it)) } + ?: Response(false, "Couldn't find note at specified path!") } } \ No newline at end of file From a5bba6575489db35c9d2f4eafb206cbdd5efe213 Mon Sep 17 00:00:00 2001 From: Nat Karmios Date: Tue, 17 Aug 2021 20:21:52 +0100 Subject: [PATCH 06/12] Restructure external action handlers --- .../external/actionhandlers/EditNotes.kt | 12 ++++++++---- .../ExternalAccessActionHandler.kt | 18 ++++++++++++++++-- .../external/actionhandlers/GetOrgInfo.kt | 17 +++++++---------- .../external/actionhandlers/ManageWidgets.kt | 9 ++++----- .../external/actionhandlers/RunSearch.kt | 7 +++---- 5 files changed, 38 insertions(+), 25 deletions(-) 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 754c28715..65fe506ed 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 @@ -13,12 +13,12 @@ import com.orgzly.android.ui.note.NotePayload import com.orgzly.org.OrgProperties class EditNotes : ExternalAccessActionHandler() { - override val actions = mapOf( - "ADD_NOTES" to ::addNote + override val actions = listOf( + action(::addNote, "ADD_NOTE", "ADD_NOTES"), + action(::deleteNote, "DELETE_NOTE", "DELETE_NOTES") ) - @Suppress("UNUSED_PARAMETER") - fun addNote(intent: Intent, context: Context): Response { + fun addNote(intent: Intent): Response { val book = getBook(intent) ?: return Response(false, "Couldn't find specified book") val newNote = notePayloadFromJson(intent.getStringExtra("PAYLOAD") ?: "") ?: return Response(false, "Invalid payload") @@ -34,6 +34,8 @@ class EditNotes : ExternalAccessActionHandler() { return Response(true, null) } + // + private fun JsonObject.getString(name: String) = this[name]?.let { if (it.isJsonPrimitive && it.asJsonPrimitive.isString) it.asJsonPrimitive.asString @@ -78,4 +80,6 @@ class EditNotes : ExternalAccessActionHandler() { ) } catch (e: NullPointerException) { null } } + + // } \ 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 ed411b874..7206894b0 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 @@ -16,15 +16,29 @@ abstract class ExternalAccessActionHandler { App.appComponent.inject(this) } - abstract val actions: Map Response> + abstract val actions: List Response>>> private val fullNameActions by lazy { - actions.mapKeys { (key, _) -> "com.orgzly.android.$key" } + 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 action(f: (Intent, Context) -> Response, vararg names: String) = names.map { it to f } + + @JvmName("intentAction") + fun action(f: (Intent) -> Response, vararg names: String) = + action({ i, _ -> f(i) }, *names) + + @JvmName("contextAction") + fun action(f: (Context) -> Response, vararg names: String) = + action({ _, c -> f(c) }, *names) + + fun action(f: () -> Response, vararg names: String) = + action({ _, _ -> f() }, *names) + + fun handle(intent: Intent, context: Context) = fullNameActions[intent.action!!]?.let { it(intent, context) } } \ No newline at end of file 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 e9e331b1b..ab51f8895 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 @@ -5,28 +5,25 @@ import android.content.Intent import com.orgzly.android.external.types.* class GetOrgInfo : ExternalAccessActionHandler() { - override val actions = mapOf( - "GET_BOOKS" to ::getBooks, - "GET_SAVED_SEARCHES" to ::getSavedSearches, - "GET_NOTE" to ::getNote + override val actions = listOf( + action(::getBooks, "GET_BOOKS"), + action(::getSavedSearches, "GET_SAVED_SEARCHES"), + action(::getNote, "GET_NOTE") ) - @Suppress("UNUSED_PARAMETER") - private fun getBooks(intent: Intent, context: Context) = Response( + private fun getBooks() = Response( true, dataRepository.getBooks() .map(Book::from).toTypedArray() ) - @Suppress("UNUSED_PARAMETER") - private fun getSavedSearches(intent: Intent, context: Context) = Response( + private fun getSavedSearches() = Response( true, dataRepository.getSavedSearches() .map(SavedSearch::from).toTypedArray() ) - @Suppress("UNUSED_PARAMETER") - private fun getNote(intent: Intent, context: Context): Response { + 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!") 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 0e805acdd..ede400f3b 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 @@ -9,13 +9,12 @@ import com.orgzly.android.external.types.Response import com.orgzly.android.widgets.ListWidgetProvider class ManageWidgets : ExternalAccessActionHandler() { - override val actions = mapOf( - "GET_WIDGETS" to ::getWidgets, - "SET_WIDGET" to ::setWidget + override val actions = listOf( + action(::getWidgets, "GET_WIDGETS"), + action(::setWidget, "SET_WIDGET") ) - @Suppress("UNUSED_PARAMETER") - private fun getWidgets(intent: Intent, context: Context): Response { + private fun getWidgets(context: Context): Response { val widgetManager = AppWidgetManager.getInstance(context) val componentName = ComponentName(context.packageName, ListWidgetProvider::class.java.name) val widgetData = widgetManager.getAppWidgetIds(componentName) diff --git a/app/src/main/java/com/orgzly/android/external/actionhandlers/RunSearch.kt b/app/src/main/java/com/orgzly/android/external/actionhandlers/RunSearch.kt index 10d505dc2..e500f72c4 100644 --- a/app/src/main/java/com/orgzly/android/external/actionhandlers/RunSearch.kt +++ b/app/src/main/java/com/orgzly/android/external/actionhandlers/RunSearch.kt @@ -6,12 +6,11 @@ import com.orgzly.android.query.user.InternalQueryParser import com.orgzly.android.external.types.* class RunSearch : ExternalAccessActionHandler() { - override val actions = mapOf( - "SEARCH" to ::runSearch + override val actions = listOf( + action(::runSearch, "SEARCH") ) - @Suppress("UNUSED_PARAMETER") - private fun runSearch(intent: Intent, context: Context): Response { + private fun runSearch(intent: Intent): Response { val searchTerm = intent.getStringExtra("QUERY") if (searchTerm.isNullOrBlank()) return Response(false, "Invalid search term!") val query = InternalQueryParser().parse(searchTerm) From 3b516c21c32f201039f1037f4da8affeb00958e7 Mon Sep 17 00:00:00 2001 From: Nat Karmios Date: Wed, 18 Aug 2021 04:07:27 +0100 Subject: [PATCH 07/12] Add remaining "essential" external actions --- .../com/orgzly/android/data/DataRepository.kt | 16 +-- .../external/ExternalAccessReceiver.kt | 1 + .../external/actionhandlers/EditNotes.kt | 112 +++++++----------- .../actionhandlers/EditSavedSearches.kt | 48 ++++++++ .../ExternalAccessActionHandler.kt | 106 ++++++++++++++++- .../external/actionhandlers/GetOrgInfo.kt | 9 +- .../external/actionhandlers/ManageWidgets.kt | 8 +- .../com/orgzly/android/external/types/Note.kt | 2 + .../orgzly/android/external/types/Response.kt | 2 +- 9 files changed, 215 insertions(+), 89 deletions(-) create mode 100644 app/src/main/java/com/orgzly/android/external/actionhandlers/EditSavedSearches.kt 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 f91234b20..b4b234579 100644 --- a/app/src/main/java/com/orgzly/android/data/DataRepository.kt +++ b/app/src/main/java/com/orgzly/android/data/DataRepository.kt @@ -613,7 +613,7 @@ class DataRepository @Inject constructor( } else { db.runInTransaction(Callable { - moveSubtrees(noteIds, Place.UNDER, target.noteId) + moveSubtrees(noteIds, target.place, target.noteId) }) } } @@ -1281,12 +1281,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..2c9c54cfb 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"), + 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 From 38f459dfdb181e19b395c9271919ee88c57de283 Mon Sep 17 00:00:00 2001 From: Nat Karmios Date: Tue, 24 Aug 2021 16:33:22 +0100 Subject: [PATCH 08/12] Tweak external receiver path handling --- .../com/orgzly/android/data/DataRepository.kt | 24 +++++++---- .../external/actionhandlers/EditNotes.kt | 2 +- .../ExternalAccessActionHandler.kt | 43 ++++++++----------- 3 files changed, 35 insertions(+), 34 deletions(-) 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 b4b234579..f427efb05 100644 --- a/app/src/main/java/com/orgzly/android/data/DataRepository.kt +++ b/app/src/main/java/com/orgzly/android/data/DataRepository.kt @@ -1280,15 +1280,21 @@ class DataRepository @Inject constructor( return db.note().getNoteAndAncestors(noteId) } - fun getNoteAtPath(bookName: String, path: String) = - 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 getNoteAtPath(fullPath: String): NoteView? { + val (bookName, path) = run { + val pathParts = fullPath.split("/") + if (pathParts.isEmpty()) return null + pathParts[0] to pathParts.drop(1).joinToString("/") + } + return 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/actionhandlers/EditNotes.kt b/app/src/main/java/com/orgzly/android/external/actionhandlers/EditNotes.kt index 2c9c54cfb..28e47bbc5 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 @@ -14,7 +14,7 @@ class EditNotes : ExternalAccessActionHandler() { private fun addNote(intent: Intent): Response { val place = intent.getNotePlace() - ?: return Response(false, "Could not find parent note") + ?: return Response(false, "Could not find parent note/book") val newNote = intent.getNotePayload() ?: return Response(false, "Invalid payload") val note = dataRepository.createNote(newNote, place) 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 c5f4b4fa3..09ac782f5 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 @@ -8,7 +8,6 @@ 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 @@ -22,6 +21,7 @@ abstract class ExternalAccessActionHandler { lateinit var dataRepository: DataRepository init { + @Suppress("LeakingThis") App.appComponent.inject(this) } @@ -59,37 +59,32 @@ abstract class ExternalAccessActionHandler { } 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.getNotePlace() = getNote(prefix="PARENT_")?.let { noteView -> + val place = try { + Place.valueOf(getStringExtra("PLACEMENT") ?: "") + } catch (e: IllegalArgumentException) { Place.UNDER } + dataRepository.getBook(noteView.bookName)?.let { book -> + NotePlace(book.id, noteView.note.id, place) + } + } ?: getBook(prefix="PARENT")?.let { book -> NotePlace(book.id) } 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() + val path = + if (allowSingle) + getStringExtra("NOTE_PATH") + ?.let { dataRepository.getNoteAtPath(it)?.note?.id } + else null + val paths = (getStringArrayExtra("NOTE_PATHS") ?: emptyArray()) + .mapNotNull { dataRepository.getNoteAtPath(it)?.note?.id } + .toTypedArray() return listOfNotNull(id, *ids, path, *paths).filter { it >= 0 }.toSet() } - fun Intent.getNote(book: Book? = null, prefix: String = "") = + fun Intent.getNote(prefix: String = "") = dataRepository.getNoteView(getLongExtra("${prefix}NOTE_ID", -1)) - ?: (book ?: getBook(prefix=prefix)) - ?.let { dataRepository.getNoteAtPath(it.name, - getStringExtra("${prefix}NOTE_PATH") ?: "") } + ?: dataRepository.getNoteAtPath(getStringExtra("${prefix}NOTE_PATH") ?: "") fun Intent.getBook(prefix: String = "") = dataRepository.getBook(getLongExtra("${prefix}BOOK_ID", -1)) From 42d49fe5bae48b29e366b1dd18a56ac8551964e5 Mon Sep 17 00:00:00 2001 From: Nat Karmios Date: Tue, 24 Aug 2021 17:42:03 +0100 Subject: [PATCH 09/12] Clean up external receiver error handling --- .../external/actionhandlers/EditNotes.kt | 42 +++++-------- .../actionhandlers/EditSavedSearches.kt | 51 ++++++++-------- .../ExternalAccessActionHandler.kt | 61 +++++++++++++------ .../external/actionhandlers/GetOrgInfo.kt | 18 ++---- .../external/actionhandlers/ManageWidgets.kt | 15 ++--- .../external/actionhandlers/RunSearch.kt | 10 +-- .../external/types/ExternalHandlerFailure.kt | 3 + 7 files changed, 101 insertions(+), 99 deletions(-) create mode 100644 app/src/main/java/com/orgzly/android/external/types/ExternalHandlerFailure.kt 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 28e47bbc5..6b29b1f0c 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,7 +1,7 @@ package com.orgzly.android.external.actionhandlers import android.content.Intent -import com.orgzly.android.external.types.Response +import com.orgzly.android.external.types.ExternalHandlerFailure class EditNotes : ExternalAccessActionHandler() { override val actions = listOf( @@ -12,52 +12,42 @@ class EditNotes : ExternalAccessActionHandler() { action(::deleteNote, "DELETE_NOTE", "DELETE_NOTES") ) - private fun addNote(intent: Intent): Response { + private fun addNote(intent: Intent): String { val place = intent.getNotePlace() - ?: return Response(false, "Could not find parent note/book") val newNote = intent.getNotePayload() - ?: return Response(false, "Invalid payload") val note = dataRepository.createNote(newNote, place) - return Response(true, "${note.id}") + return "${note.id}" } - private fun editNote(intent: Intent): Response { + private fun editNote(intent: Intent) { 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 fun refileNote(intent: Intent): Response { + private fun refileNote(intent: Intent) { 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 moveNote(intent: Intent): Response { + private fun moveNote(intent: Intent) { 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) } + "UP" -> moveNote(intent.getBook().id, notes, -1) + "DOWN" -> moveNote(intent.getBook().id, notes, 1) "LEFT" -> promoteNotes(notes) "RIGHT" -> demoteNotes(notes) - else -> return Response(false, "Invalid direction") + else -> throw ExternalHandlerFailure("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() + private fun deleteNote(intent: Intent) { + intent.getNoteIds().groupBy { + dataRepository.getNoteView(it)?.bookName + ?: throw ExternalHandlerFailure("invalid note id $it") + }.forEach { (bookName, notes) -> + dataRepository.deleteNotes(dataRepository.getBook(bookName)!!.id, notes.toSet()) + } } } \ 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 index 6df8c05d9..38013d304 100644 --- a/app/src/main/java/com/orgzly/android/external/actionhandlers/EditSavedSearches.kt +++ b/app/src/main/java/com/orgzly/android/external/actionhandlers/EditSavedSearches.kt @@ -2,7 +2,7 @@ package com.orgzly.android.external.actionhandlers import android.content.Intent import com.orgzly.android.db.entity.SavedSearch -import com.orgzly.android.external.types.Response +import com.orgzly.android.external.types.ExternalHandlerFailure class EditSavedSearches : ExternalAccessActionHandler() { override val actions = listOf( @@ -12,37 +12,36 @@ class EditSavedSearches : ExternalAccessActionHandler() { 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 addSavedSearch(intent: Intent): String { + val savedSearch = intent.getNewSavedSearch() + val id = dataRepository.createSavedSearch(savedSearch) + return "$id" + } - 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 editSavedSearch(intent: Intent) { + val savedSearch = intent.getSavedSearch() + val newSavedSearch = intent.getNewSavedSearch(allowBlank = true) + 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 + )) + } - private fun moveSavedSearch(intent: Intent) = intent.getSavedSearch()?.let { savedSearch -> + private fun moveSavedSearch(intent: Intent) { + val savedSearch = intent.getSavedSearch() when (intent.getStringExtra("DIRECTION")) { "UP" -> dataRepository.moveSavedSearchUp(savedSearch.id) "DOWN" -> dataRepository.moveSavedSearchDown(savedSearch.id) - else -> return Response(false, "Invalid direction") + else -> throw ExternalHandlerFailure("invalid direction") } - return Response() - } ?: Response(false, "Couldn't find saved search") + } - private fun deleteSavedSearch(intent: Intent) = intent.getSavedSearch()?.let { savedSearch -> + private fun deleteSavedSearch(intent: Intent) { + val savedSearch = intent.getSavedSearch() 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 09ac782f5..f7080ff0c 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 @@ -9,6 +9,7 @@ import com.google.gson.JsonParser import com.orgzly.android.App import com.orgzly.android.data.DataRepository import com.orgzly.android.db.entity.SavedSearch +import com.orgzly.android.external.types.ExternalHandlerFailure import com.orgzly.android.external.types.Response import com.orgzly.android.ui.NotePlace import com.orgzly.android.ui.Place @@ -25,12 +26,12 @@ abstract class ExternalAccessActionHandler { App.appComponent.inject(this) } - abstract val actions: List Response>>> + abstract val actions: List Any>>> private val fullNameActions by lazy { actions.flatten().toMap().mapKeys { (key, _) -> "com.orgzly.android.$key" } } - fun Intent.getNotePayload(title: String? = null): NotePayload? { + fun Intent.getNotePayload(title: String? = null): NotePayload { val rawJson = getStringExtra("NOTE_PAYLOAD") val json = try { JsonParser.parseString(rawJson) @@ -56,19 +57,27 @@ abstract class ExternalAccessActionHandler { json["properties"]?.asMap?.forEach { (k, v) -> this[k] = v } } ) - } catch (e: NullPointerException) { null } + } catch (e: NullPointerException) { + throw ExternalHandlerFailure("invalid payload") + } } - fun Intent.getNotePlace() = getNote(prefix="PARENT_")?.let { noteView -> - val place = try { - Place.valueOf(getStringExtra("PLACEMENT") ?: "") - } catch (e: IllegalArgumentException) { Place.UNDER } - dataRepository.getBook(noteView.bookName)?.let { book -> - NotePlace(book.id, noteView.note.id, place) + fun Intent.getNotePlace() = try { + getNote(prefix="PARENT_").let { noteView -> + val place = try { + Place.valueOf(getStringExtra("PLACEMENT") ?: "") + } catch (e: IllegalArgumentException) { Place.UNDER } + dataRepository.getBook(noteView.bookName)?.let { book -> + NotePlace(book.id, noteView.note.id, place) + } } - } ?: getBook(prefix="PARENT")?.let { book -> NotePlace(book.id) } + } catch (e: ExternalHandlerFailure) { null } ?: try { + NotePlace(getBook(prefix="PARENT_").id) + } catch (e: ExternalHandlerFailure) { + throw ExternalHandlerFailure("could not find parent note/book") + } - fun Intent.getNoteIds(allowSingle: Boolean = true): Set { + fun Intent.getNoteIds(allowSingle: Boolean = true, allowEmpty: Boolean = false): Set { val id = if (allowSingle) getLongExtra("NOTE_ID", -1) else null val ids = getLongArrayExtra("NOTE_IDS")?.toTypedArray() ?: emptyArray() val path = @@ -79,26 +88,33 @@ abstract class ExternalAccessActionHandler { val paths = (getStringArrayExtra("NOTE_PATHS") ?: emptyArray()) .mapNotNull { dataRepository.getNoteAtPath(it)?.note?.id } .toTypedArray() - return listOfNotNull(id, *ids, path, *paths).filter { it >= 0 }.toSet() + return listOfNotNull(id, *ids, path, *paths).filter { it >= 0 }.toSet().also { + if (it.isEmpty() && !allowEmpty) + throw ExternalHandlerFailure("no notes specified") + } } fun Intent.getNote(prefix: String = "") = dataRepository.getNoteView(getLongExtra("${prefix}NOTE_ID", -1)) ?: dataRepository.getNoteAtPath(getStringExtra("${prefix}NOTE_PATH") ?: "") + ?: throw ExternalHandlerFailure("couldn't find note") fun Intent.getBook(prefix: String = "") = dataRepository.getBook(getLongExtra("${prefix}BOOK_ID", -1)) ?: dataRepository.getBook(getStringExtra("${prefix}BOOK_NAME") ?: "") + ?: throw ExternalHandlerFailure("couldn't find book") fun Intent.getSavedSearch() = dataRepository.getSavedSearch(getLongExtra("SAVED_SEARCH_ID", -1)) ?: dataRepository.getSavedSearches() .find { it.name == getStringExtra("SAVED_SEARCH_NAME") } + ?: throw ExternalHandlerFailure("couldn't find saved search") - fun Intent.getNewSavedSearch(allowBlank: Boolean = false): SavedSearch? { + 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 + if (!allowBlank && (name.isNullOrBlank() || query.isNullOrBlank())) + throw ExternalHandlerFailure("invalid parameters for new saved search") return SavedSearch(0, name ?: "", query ?: "", 0) } @@ -120,20 +136,25 @@ abstract class ExternalAccessActionHandler { .toMap() } else null - fun action(f: (Intent, Context) -> Response, vararg names: String) = names.map { it to f } + fun action(f: (Intent, Context) -> Any, vararg names: String) = names.map { it to f } @JvmName("intentAction") - fun action(f: (Intent) -> Response, vararg names: String) = + fun action(f: (Intent) -> Any, vararg names: String) = action({ i, _ -> f(i) }, *names) @JvmName("contextAction") - fun action(f: (Context) -> Response, vararg names: String) = + fun action(f: (Context) -> Any, vararg names: String) = action({ _, c -> f(c) }, *names) - fun action(f: () -> Response, vararg names: String) = + fun action(f: () -> Any, vararg names: String) = action({ _, _ -> f() }, *names) - fun handle(intent: Intent, context: Context) = - fullNameActions[intent.action!!]?.let { it(intent, context) } + fun handle(intent: Intent, context: Context) = try { + fullNameActions[intent.action!!] + ?.let { it(intent, context) } + ?.let { Response(true, if (it is Unit) null else it) } + } catch (e: ExternalHandlerFailure) { + Response(false, e.message) + } } \ No newline at end of file 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 8a3406fa8..746b809f2 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 @@ -10,20 +10,12 @@ class GetOrgInfo : ExternalAccessActionHandler() { action(::getNote, "GET_NOTE") ) - private fun getBooks() = Response( - true, - dataRepository.getBooks() - .map(Book::from).toTypedArray() - ) + private fun getBooks() = + dataRepository.getBooks().map(Book::from).toTypedArray() - private fun getSavedSearches() = Response( - true, - dataRepository.getSavedSearches() - .map(SavedSearch::from).toTypedArray() - ) + private fun getSavedSearches() = + dataRepository.getSavedSearches().map(SavedSearch::from).toTypedArray() private fun getNote(intent: Intent) = - intent.getNote() - ?.let { Response(true, Note.from(it)) } - ?: Response(false, "Couldn't find note at specified path!") + Note.from(intent.getNote()) } \ 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 7b34eb0ad..0fa406910 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 @@ -5,7 +5,8 @@ import android.content.ComponentName import android.content.Context import android.content.Intent import com.orgzly.android.AppIntent -import com.orgzly.android.external.types.Response +import com.orgzly.android.db.entity.SavedSearch +import com.orgzly.android.external.types.ExternalHandlerFailure import com.orgzly.android.widgets.ListWidgetProvider class ManageWidgets : ExternalAccessActionHandler() { @@ -14,27 +15,23 @@ class ManageWidgets : ExternalAccessActionHandler() { action(::setWidget, "SET_WIDGET") ) - private fun getWidgets(context: Context): Response { + private fun getWidgets(context: Context): Map { val widgetManager = AppWidgetManager.getInstance(context) val componentName = ComponentName(context.packageName, ListWidgetProvider::class.java.name) - val widgetData = widgetManager.getAppWidgetIds(componentName) + return widgetManager.getAppWidgetIds(componentName) .map { it to ListWidgetProvider.getSavedSearch(context, it, dataRepository) } .toMap() - return Response(true, widgetData) } - private fun setWidget(intent: Intent, context: Context): Response { + private fun setWidget(intent: Intent, context: Context) { val widgetId = intent.getIntExtra("WIDGET_ID", -1) - if (widgetId < 0) return Response(false, "invalid widget ID") + if (widgetId < 0) throw ExternalHandlerFailure("invalid widget 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, savedSearch.id) }) - - return Response() } } diff --git a/app/src/main/java/com/orgzly/android/external/actionhandlers/RunSearch.kt b/app/src/main/java/com/orgzly/android/external/actionhandlers/RunSearch.kt index e500f72c4..00c4bef66 100644 --- a/app/src/main/java/com/orgzly/android/external/actionhandlers/RunSearch.kt +++ b/app/src/main/java/com/orgzly/android/external/actionhandlers/RunSearch.kt @@ -1,20 +1,20 @@ package com.orgzly.android.external.actionhandlers -import android.content.Context import android.content.Intent +import com.orgzly.android.external.types.ExternalHandlerFailure +import com.orgzly.android.external.types.Note import com.orgzly.android.query.user.InternalQueryParser -import com.orgzly.android.external.types.* class RunSearch : ExternalAccessActionHandler() { override val actions = listOf( action(::runSearch, "SEARCH") ) - private fun runSearch(intent: Intent): Response { + private fun runSearch(intent: Intent): List { val searchTerm = intent.getStringExtra("QUERY") - if (searchTerm.isNullOrBlank()) return Response(false, "Invalid search term!") + if (searchTerm.isNullOrBlank()) throw ExternalHandlerFailure("invalid search term") val query = InternalQueryParser().parse(searchTerm) val notes = dataRepository.selectNotesFromQuery(query) - return Response(true, notes.map(Note::from).toTypedArray()) + return notes.map(Note::from) } } \ No newline at end of file diff --git a/app/src/main/java/com/orgzly/android/external/types/ExternalHandlerFailure.kt b/app/src/main/java/com/orgzly/android/external/types/ExternalHandlerFailure.kt new file mode 100644 index 000000000..81030ead5 --- /dev/null +++ b/app/src/main/java/com/orgzly/android/external/types/ExternalHandlerFailure.kt @@ -0,0 +1,3 @@ +package com.orgzly.android.external.types + +class ExternalHandlerFailure(msg: String) : Exception(msg) \ No newline at end of file From b3c3057a222d8d78a7e0fe2481641315f9c61bc0 Mon Sep 17 00:00:00 2001 From: Nat Karmios Date: Fri, 17 Mar 2023 14:58:56 +0000 Subject: [PATCH 10/12] External receiver updates - Move Intent parsing to separate file - Support finding note by query (instead of selecting by ID or path) - Give properties in note info --- .../external/actionhandlers/EditNotes.kt | 2 +- .../actionhandlers/EditSavedSearches.kt | 8 +- .../ExternalAccessActionHandler.kt | 120 +-------------- .../actionhandlers/ExternalIntentParser.kt | 141 ++++++++++++++++++ .../external/actionhandlers/GetOrgInfo.kt | 4 +- .../external/actionhandlers/ManageWidgets.kt | 3 +- .../external/actionhandlers/RunSearch.kt | 5 +- .../com/orgzly/android/external/types/Note.kt | 87 ++++++----- 8 files changed, 200 insertions(+), 170 deletions(-) create mode 100644 app/src/main/java/com/orgzly/android/external/actionhandlers/ExternalIntentParser.kt 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 6b29b1f0c..94d1cd749 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 @@ -50,4 +50,4 @@ class EditNotes : ExternalAccessActionHandler() { dataRepository.deleteNotes(dataRepository.getBook(bookName)!!.id, notes.toSet()) } } -} \ 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 index 38013d304..4f8ced4ef 100644 --- a/app/src/main/java/com/orgzly/android/external/actionhandlers/EditSavedSearches.kt +++ b/app/src/main/java/com/orgzly/android/external/actionhandlers/EditSavedSearches.kt @@ -23,10 +23,8 @@ class EditSavedSearches : ExternalAccessActionHandler() { val newSavedSearch = intent.getNewSavedSearch(allowBlank = true) dataRepository.updateSavedSearch(SavedSearch( savedSearch.id, - (if (newSavedSearch.name.isBlank()) savedSearch.name - else newSavedSearch.name), - (if (newSavedSearch.query.isBlank()) savedSearch.query - else newSavedSearch.query), + newSavedSearch.name.ifBlank { savedSearch.name }, + newSavedSearch.query.ifBlank { savedSearch.query }, savedSearch.position )) } @@ -44,4 +42,4 @@ class EditSavedSearches : ExternalAccessActionHandler() { val savedSearch = intent.getSavedSearch() dataRepository.deleteSavedSearches(setOf(savedSearch.id)) } -} \ 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 f7080ff0c..7d6ffb303 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,24 +2,15 @@ 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.SavedSearch import com.orgzly.android.external.types.ExternalHandlerFailure 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 { +abstract class ExternalAccessActionHandler : ExternalIntentParser { @Inject - lateinit var dataRepository: DataRepository + override lateinit var dataRepository: DataRepository init { @Suppress("LeakingThis") @@ -31,111 +22,6 @@ abstract class ExternalAccessActionHandler { actions.flatten().toMap().mapKeys { (key, _) -> "com.orgzly.android.$key" } } - 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) { - throw ExternalHandlerFailure("invalid payload") - } - } - - fun Intent.getNotePlace() = try { - getNote(prefix="PARENT_").let { noteView -> - val place = try { - Place.valueOf(getStringExtra("PLACEMENT") ?: "") - } catch (e: IllegalArgumentException) { Place.UNDER } - dataRepository.getBook(noteView.bookName)?.let { book -> - NotePlace(book.id, noteView.note.id, place) - } - } - } catch (e: ExternalHandlerFailure) { null } ?: try { - NotePlace(getBook(prefix="PARENT_").id) - } catch (e: ExternalHandlerFailure) { - throw ExternalHandlerFailure("could not find parent note/book") - } - - fun Intent.getNoteIds(allowSingle: Boolean = true, allowEmpty: Boolean = false): Set { - val id = if (allowSingle) getLongExtra("NOTE_ID", -1) else null - val ids = getLongArrayExtra("NOTE_IDS")?.toTypedArray() ?: emptyArray() - val path = - if (allowSingle) - getStringExtra("NOTE_PATH") - ?.let { dataRepository.getNoteAtPath(it)?.note?.id } - else null - val paths = (getStringArrayExtra("NOTE_PATHS") ?: emptyArray()) - .mapNotNull { dataRepository.getNoteAtPath(it)?.note?.id } - .toTypedArray() - return listOfNotNull(id, *ids, path, *paths).filter { it >= 0 }.toSet().also { - if (it.isEmpty() && !allowEmpty) - throw ExternalHandlerFailure("no notes specified") - } - } - - fun Intent.getNote(prefix: String = "") = - dataRepository.getNoteView(getLongExtra("${prefix}NOTE_ID", -1)) - ?: dataRepository.getNoteAtPath(getStringExtra("${prefix}NOTE_PATH") ?: "") - ?: throw ExternalHandlerFailure("couldn't find note") - - fun Intent.getBook(prefix: String = "") = - dataRepository.getBook(getLongExtra("${prefix}BOOK_ID", -1)) - ?: dataRepository.getBook(getStringExtra("${prefix}BOOK_NAME") ?: "") - ?: throw ExternalHandlerFailure("couldn't find book") - - fun Intent.getSavedSearch() = - dataRepository.getSavedSearch(getLongExtra("SAVED_SEARCH_ID", -1)) - ?: dataRepository.getSavedSearches() - .find { it.name == getStringExtra("SAVED_SEARCH_NAME") } - ?: throw ExternalHandlerFailure("couldn't find saved search") - - 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())) - throw ExternalHandlerFailure("invalid parameters for new saved search") - 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) -> Any, vararg names: String) = names.map { it to f } @JvmName("intentAction") @@ -157,4 +43,4 @@ abstract class ExternalAccessActionHandler { } catch (e: ExternalHandlerFailure) { Response(false, e.message) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/orgzly/android/external/actionhandlers/ExternalIntentParser.kt b/app/src/main/java/com/orgzly/android/external/actionhandlers/ExternalIntentParser.kt new file mode 100644 index 000000000..ecbf9d635 --- /dev/null +++ b/app/src/main/java/com/orgzly/android/external/actionhandlers/ExternalIntentParser.kt @@ -0,0 +1,141 @@ +package com.orgzly.android.external.actionhandlers + +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.data.DataRepository +import com.orgzly.android.db.entity.NoteView +import com.orgzly.android.db.entity.SavedSearch +import com.orgzly.android.external.types.ExternalHandlerFailure +import com.orgzly.android.query.user.InternalQueryParser +import com.orgzly.android.ui.NotePlace +import com.orgzly.android.ui.Place +import com.orgzly.android.ui.note.NotePayload +import com.orgzly.org.OrgProperties + +interface ExternalIntentParser { + val dataRepository: DataRepository + + 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) { + throw ExternalHandlerFailure("invalid payload") + } + } + + private fun getNoteByQuery(rawQuery: String?): NoteView { + if (rawQuery == null) + throw ExternalHandlerFailure("couldn't find note") + val query = InternalQueryParser().parse(rawQuery) + val notes = dataRepository.selectNotesFromQuery(query) + if (notes.isEmpty()) + throw ExternalHandlerFailure("couldn't find note") + if (notes.size > 1) + throw ExternalHandlerFailure("query \"$rawQuery\" gave multiple results") + return notes[0] + } + + fun Intent.getNote(prefix: String = "") = + dataRepository.getNoteView(getLongExtra("${prefix}NOTE_ID", -1)) + ?: dataRepository.getNoteAtPath(getStringExtra("${prefix}NOTE_PATH") ?: "") + ?: getNoteByQuery(getStringExtra("${prefix}NOTE_QUERY")) + + fun Intent.getNoteAndProps(prefix: String = "") = getNote(prefix).let { + it to dataRepository.getNoteProperties(it.note.id) + } + + fun Intent.getBook(prefix: String = "") = + dataRepository.getBook(getLongExtra("${prefix}BOOK_ID", -1)) + ?: dataRepository.getBook(getStringExtra("${prefix}BOOK_NAME") ?: "") + ?: throw ExternalHandlerFailure("couldn't find book") + + fun Intent.getNotePlace() = try { + getNote(prefix="PARENT_").let { noteView -> + val place = try { + Place.valueOf(getStringExtra("PLACEMENT") ?: "") + } catch (e: IllegalArgumentException) { Place.UNDER } + dataRepository.getBook(noteView.bookName)?.let { book -> + NotePlace(book.id, noteView.note.id, place) + } + } + } catch (e: ExternalHandlerFailure) { null } ?: try { + NotePlace(getBook(prefix="PARENT_").id) + } catch (e: ExternalHandlerFailure) { + throw ExternalHandlerFailure("couldn't find parent note/book") + } + + fun Intent.getNoteIds(allowSingle: Boolean = true, allowEmpty: Boolean = false): Set { + val id = if (allowSingle) getLongExtra("NOTE_ID", -1) else null + val ids = getLongArrayExtra("NOTE_IDS")?.toTypedArray() ?: emptyArray() + val path = + if (allowSingle) + getStringExtra("NOTE_PATH") + ?.let { dataRepository.getNoteAtPath(it)?.note?.id } + else null + val paths = (getStringArrayExtra("NOTE_PATHS") ?: emptyArray()) + .mapNotNull { dataRepository.getNoteAtPath(it)?.note?.id } + .toTypedArray() + return listOfNotNull(id, *ids, path, *paths).filter { it >= 0 }.toSet().also { + if (it.isEmpty() && !allowEmpty) + throw ExternalHandlerFailure("no notes specified") + } + } + + fun Intent.getSavedSearch() = + dataRepository.getSavedSearch(getLongExtra("SAVED_SEARCH_ID", -1)) + ?: dataRepository.getSavedSearches() + .find { it.name == getStringExtra("SAVED_SEARCH_NAME") } + ?: throw ExternalHandlerFailure("couldn't find saved search") + + 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())) + throw ExternalHandlerFailure("invalid parameters for new saved search") + 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 +} 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 746b809f2..2f7442f41 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 @@ -17,5 +17,5 @@ class GetOrgInfo : ExternalAccessActionHandler() { dataRepository.getSavedSearches().map(SavedSearch::from).toTypedArray() private fun getNote(intent: Intent) = - Note.from(intent.getNote()) -} \ No newline at end of file + Note.from(intent.getNoteAndProps()) +} 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 0fa406910..fb9374bfc 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 @@ -19,8 +19,7 @@ class ManageWidgets : ExternalAccessActionHandler() { val widgetManager = AppWidgetManager.getInstance(context) val componentName = ComponentName(context.packageName, ListWidgetProvider::class.java.name) return widgetManager.getAppWidgetIds(componentName) - .map { it to ListWidgetProvider.getSavedSearch(context, it, dataRepository) } - .toMap() + .associateWith { ListWidgetProvider.getSavedSearch(context, it, dataRepository) } } private fun setWidget(intent: Intent, context: Context) { diff --git a/app/src/main/java/com/orgzly/android/external/actionhandlers/RunSearch.kt b/app/src/main/java/com/orgzly/android/external/actionhandlers/RunSearch.kt index 00c4bef66..6542f7cc0 100644 --- a/app/src/main/java/com/orgzly/android/external/actionhandlers/RunSearch.kt +++ b/app/src/main/java/com/orgzly/android/external/actionhandlers/RunSearch.kt @@ -15,6 +15,7 @@ class RunSearch : ExternalAccessActionHandler() { if (searchTerm.isNullOrBlank()) throw ExternalHandlerFailure("invalid search term") val query = InternalQueryParser().parse(searchTerm) val notes = dataRepository.selectNotesFromQuery(query) - return notes.map(Note::from) + val notesWithProps = notes.map { it to dataRepository.getNoteProperties(it.note.id) } + return notesWithProps.map(Note::from) } -} \ No newline at end of file +} 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 eedba1876..9bba97229 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 @@ -1,5 +1,6 @@ package com.orgzly.android.external.types +import com.orgzly.android.db.entity.NoteProperty import com.orgzly.android.db.entity.NoteView data class Note( @@ -14,59 +15,63 @@ data class Note( val closed: Timestamp?, val priority: String?, val state: String?, - val createdAt: Long? - + val createdAt: Long?, + val properties: Map ) { companion object { - fun from(view: NoteView): Note { + fun from(view: NoteView, props: List): Note { val note = view.note return Note( - note.id, - note.title, - note.content, - note.tags?.split(" +".toRegex()) - ?.filter { it.isNotEmpty() } - ?: emptyList(), - view.getInheritedTagsList() - .filter { it.isNotEmpty() }, - view.bookName, - Timestamp.from( - view.scheduledTimeTimestamp, - view.scheduledTimeString, - view.scheduledTimeEndString, - view.scheduledRangeString - ), - Timestamp.from( - view.deadlineTimeTimestamp, - view.deadlineTimeString, - view.deadlineTimeEndString, - view.deadlineRangeString - ), - Timestamp.from( - view.closedTimeTimestamp, - view.closedTimeString, - view.closedTimeEndString, - view.closedRangeString - ), - note.priority, - note.state, - note.createdAt + note.id, + note.title, + note.content, + note.tags?.split(" +".toRegex()) + ?.filter { it.isNotEmpty() } + ?: emptyList(), + view.getInheritedTagsList() + .filter { it.isNotEmpty() }, + view.bookName, + Timestamp.from( + view.scheduledTimeTimestamp, + view.scheduledTimeString, + view.scheduledTimeEndString, + view.scheduledRangeString + ), + Timestamp.from( + view.deadlineTimeTimestamp, + view.deadlineTimeString, + view.deadlineTimeEndString, + view.deadlineRangeString + ), + Timestamp.from( + view.closedTimeTimestamp, + view.closedTimeString, + view.closedTimeEndString, + view.closedRangeString + ), + note.priority, + note.state, + note.createdAt, + props.associate { it.name to it.value } ) } + + fun from(noteAndProps: Pair>) = + from(noteAndProps.first, noteAndProps.second) } data class Timestamp( - val timeTimestamp: Long, - val timeString: String, - val timeEndString: String? = null, - val rangeString: String?, + val timeTimestamp: Long, + val timeString: String, + val timeEndString: String? = null, + val rangeString: String?, ) { companion object { fun from( - timeTimestamp: Long?, - timeString: String?, - timeEndString: String?, - rangeString: String? + timeTimestamp: Long?, + timeString: String?, + timeEndString: String?, + rangeString: String? ): Timestamp? { return if (timeTimestamp != null && timeString != null) Timestamp(timeTimestamp, timeString, timeEndString, rangeString) From fc2a1dff09b6be3a6d5338fe952f2f1e5ad75e68 Mon Sep 17 00:00:00 2001 From: Nat Karmios Date: Sat, 18 Mar 2023 01:06:00 +0000 Subject: [PATCH 11/12] External receiver: prioritise rangeString in timestamp --- .../com/orgzly/android/external/types/Note.kt | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 9bba97229..66c3fd03b 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 @@ -32,22 +32,22 @@ data class Note( .filter { it.isNotEmpty() }, view.bookName, Timestamp.from( + view.scheduledRangeString, view.scheduledTimeTimestamp, view.scheduledTimeString, view.scheduledTimeEndString, - view.scheduledRangeString ), Timestamp.from( + view.deadlineRangeString, view.deadlineTimeTimestamp, view.deadlineTimeString, view.deadlineTimeEndString, - view.deadlineRangeString ), Timestamp.from( + view.closedRangeString, view.closedTimeTimestamp, view.closedTimeString, view.closedTimeEndString, - view.closedRangeString ), note.priority, note.state, @@ -61,20 +61,20 @@ data class Note( } data class Timestamp( + val rangeString: String, val timeTimestamp: Long, - val timeString: String, + val timeString: String?, val timeEndString: String? = null, - val rangeString: String?, ) { companion object { fun from( + rangeString: String?, timeTimestamp: Long?, timeString: String?, timeEndString: String?, - rangeString: String? ): Timestamp? { - return if (timeTimestamp != null && timeString != null) - Timestamp(timeTimestamp, timeString, timeEndString, rangeString) + return if (rangeString != null && timeTimestamp != null) + Timestamp(rangeString, timeTimestamp, timeString, timeEndString) else null } } From a5d3b96ffdf1cc9f9a0939d5162c6e8ab1061c8e Mon Sep 17 00:00:00 2001 From: Nat Karmios Date: Sat, 18 Mar 2023 01:08:48 +0000 Subject: [PATCH 12/12] Improve external handler error reporting --- .../ExternalAccessActionHandler.kt | 3 +- .../actionhandlers/ExternalIntentParser.kt | 44 ++++++++----------- 2 files changed, 20 insertions(+), 27 deletions(-) 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 7d6ffb303..a7cce4f05 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 @@ -4,7 +4,6 @@ import android.content.Context import android.content.Intent import com.orgzly.android.App import com.orgzly.android.data.DataRepository -import com.orgzly.android.external.types.ExternalHandlerFailure import com.orgzly.android.external.types.Response import javax.inject.Inject @@ -40,7 +39,7 @@ abstract class ExternalAccessActionHandler : ExternalIntentParser { fullNameActions[intent.action!!] ?.let { it(intent, context) } ?.let { Response(true, if (it is Unit) null else it) } - } catch (e: ExternalHandlerFailure) { + } catch (e: Exception) { Response(false, e.message) } } diff --git a/app/src/main/java/com/orgzly/android/external/actionhandlers/ExternalIntentParser.kt b/app/src/main/java/com/orgzly/android/external/actionhandlers/ExternalIntentParser.kt index ecbf9d635..f8478c30b 100644 --- a/app/src/main/java/com/orgzly/android/external/actionhandlers/ExternalIntentParser.kt +++ b/app/src/main/java/com/orgzly/android/external/actionhandlers/ExternalIntentParser.kt @@ -3,7 +3,6 @@ package com.orgzly.android.external.actionhandlers 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.data.DataRepository import com.orgzly.android.db.entity.NoteView @@ -22,31 +21,26 @@ interface ExternalIntentParser { 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) { - throw ExternalHandlerFailure("invalid payload") + .let { if (it.isJsonObject) it.asJsonObject else null }!! + } catch (e: Exception) { + throw ExternalHandlerFailure("failed to parse json: ${e.message}\n$rawJson") } + return NotePayload( + json.getString("title") ?: title + ?: throw ExternalHandlerFailure("no title supplied!\n$rawJson"), + 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 } + } + ) } private fun getNoteByQuery(rawQuery: String?): NoteView {