From d0b5d0489281ab705b6f41e5a232fcdaf5a4ae9f Mon Sep 17 00:00:00 2001 From: Michael Kadziela Date: Wed, 25 Jun 2025 17:54:22 +1000 Subject: [PATCH] Add the ability to create action types and actions with only Rust so you can register them; fix Android incorrectly storing actions in an action group --- .../src/main/java/NotificationStorage.kt | 166 ++++++++--------- plugins/notification/src/models.rs | 172 ++++++++++++++++++ 2 files changed, 256 insertions(+), 82 deletions(-) diff --git a/plugins/notification/android/src/main/java/NotificationStorage.kt b/plugins/notification/android/src/main/java/NotificationStorage.kt index bceb985d..15da4e5e 100644 --- a/plugins/notification/android/src/main/java/NotificationStorage.kt +++ b/plugins/notification/android/src/main/java/NotificationStorage.kt @@ -7,8 +7,8 @@ package app.tauri.notification import android.content.Context import android.content.SharedPreferences import com.fasterxml.jackson.databind.ObjectMapper -import org.json.JSONException import java.lang.Exception +import org.json.JSONException // Key for private preferences private const val NOTIFICATION_STORE_ID = "NOTIFICATION_STORE" @@ -16,98 +16,100 @@ private const val NOTIFICATION_STORE_ID = "NOTIFICATION_STORE" private const val ACTION_TYPES_ID = "ACTION_TYPE_STORE" class NotificationStorage(private val context: Context, private val jsonMapper: ObjectMapper) { - fun appendNotifications(localNotifications: List) { - val storage = getStorage(NOTIFICATION_STORE_ID) - val editor = storage.edit() - for (request in localNotifications) { - if (request.schedule != null) { - val key: String = request.id.toString() - editor.putString(key, request.sourceJson.toString()) - } + fun appendNotifications(localNotifications: List) { + val storage = getStorage(NOTIFICATION_STORE_ID) + val editor = storage.edit() + for (request in localNotifications) { + if (request.schedule != null) { + val key: String = request.id.toString() + editor.putString(key, request.sourceJson.toString()) + } + } + editor.apply() } - editor.apply() - } - fun getSavedNotificationIds(): List { - val storage = getStorage(NOTIFICATION_STORE_ID) - val all = storage.all - return if (all != null) { - ArrayList(all.keys) - } else ArrayList() - } + fun getSavedNotificationIds(): List { + val storage = getStorage(NOTIFICATION_STORE_ID) + val all = storage.all + return if (all != null) { + ArrayList(all.keys) + } else ArrayList() + } - fun getSavedNotifications(): List { - val storage = getStorage(NOTIFICATION_STORE_ID) - val all = storage.all - if (all != null) { - val notifications = ArrayList() - for (key in all.keys) { - val notificationString = all[key] as String? - try { - val notification = jsonMapper.readValue(notificationString, Notification::class.java) - notifications.add(notification) - } catch (_: Exception) { } - } - return notifications + fun getSavedNotifications(): List { + val storage = getStorage(NOTIFICATION_STORE_ID) + val all = storage.all + if (all != null) { + val notifications = ArrayList() + for (key in all.keys) { + val notificationString = all[key] as String? + try { + val notification = + jsonMapper.readValue(notificationString, Notification::class.java) + notifications.add(notification) + } catch (_: Exception) {} + } + return notifications + } + return ArrayList() } - return ArrayList() - } - fun getSavedNotification(key: String): Notification? { - val storage = getStorage(NOTIFICATION_STORE_ID) - val notificationString = try { - storage.getString(key, null) - } catch (ex: ClassCastException) { - return null - } ?: return null + fun getSavedNotification(key: String): Notification? { + val storage = getStorage(NOTIFICATION_STORE_ID) + val notificationString = + try { + storage.getString(key, null) + } catch (ex: ClassCastException) { + return null + } ?: return null - return try { - jsonMapper.readValue(notificationString, Notification::class.java) - } catch (ex: JSONException) { - null + return try { + jsonMapper.readValue(notificationString, Notification::class.java) + } catch (ex: JSONException) { + null + } } - } - fun deleteNotification(id: String?) { - val editor = getStorage(NOTIFICATION_STORE_ID).edit() - editor.remove(id) - editor.apply() - } + fun deleteNotification(id: String?) { + val editor = getStorage(NOTIFICATION_STORE_ID).edit() + editor.remove(id) + editor.apply() + } - private fun getStorage(key: String): SharedPreferences { - return context.getSharedPreferences(key, Context.MODE_PRIVATE) - } + private fun getStorage(key: String): SharedPreferences { + return context.getSharedPreferences(key, Context.MODE_PRIVATE) + } - fun writeActionGroup(actions: List) { - for (type in actions) { - val i = type.id - val editor = getStorage(ACTION_TYPES_ID + type.id).edit() - editor.clear() - editor.putInt("count", type.actions.size) - for (action in type.actions) { - editor.putString("id$i", action.id) - editor.putString("title$i", action.title) - editor.putBoolean("input$i", action.input ?: false) - } - editor.apply() + fun writeActionGroup(actions: List) { + for (type in actions) { + val editor = getStorage(ACTION_TYPES_ID + type.id).edit() + editor.clear() + editor.putInt("count", type.actions.size) + for (i in 0 until type.actions.size) { + val action = type.actions[i] + editor.putString("id$i", action.id) + editor.putString("title$i", action.title) + editor.putBoolean("input$i", action.input ?: false) + } + editor.apply() + } } - } - fun getActionGroup(forId: String): Array { - val storage = getStorage(ACTION_TYPES_ID + forId) - val count = storage.getInt("count", 0) - val actions: Array = arrayOfNulls(count) - for (i in 0 until count) { - val id = storage.getString("id$i", "") - val title = storage.getString("title$i", "") - val input = storage.getBoolean("input$i", false) + fun getActionGroup(forId: String): Array { + val storage = getStorage(ACTION_TYPES_ID + forId) + val count = storage.getInt("count", 0) + val actions: Array = arrayOfNulls(count) + for (i in 0 until count) { + val id = storage.getString("id$i", "") + val title = storage.getString("title$i", "") + val input = storage.getBoolean("input$i", false) - val action = NotificationAction() - action.id = id ?: "" - action.title = title - action.input = input - actions[i] = action + val action = NotificationAction() + action.id = id ?: "" + action.title = title + action.input = input + actions[i] = action + } + return actions } - return actions - } -} \ No newline at end of file +} diff --git a/plugins/notification/src/models.rs b/plugins/notification/src/models.rs index 02134e4d..3ebbd608 100644 --- a/plugins/notification/src/models.rs +++ b/plugins/notification/src/models.rs @@ -320,6 +320,92 @@ pub struct ActionType { hidden_previews_show_subtitle: bool, } +#[derive(Debug)] +pub struct ActionTypeBuilder(ActionType); + +impl ActionType { + pub fn builder(id: impl Into) -> ActionTypeBuilder { + ActionTypeBuilder(Self { + id: id.into(), + actions: Vec::new(), + hidden_previews_body_placeholder: None, + custom_dismiss_action: false, + allow_in_car_play: false, + hidden_previews_show_title: false, + hidden_previews_show_subtitle: false, + }) + } + + pub fn id(&self) -> &str { + &self.id + } + + pub fn actions(&self) -> &[Action] { + &self.actions + } + + pub fn hidden_previews_body_placeholder(&self) -> Option<&str> { + self.hidden_previews_body_placeholder.as_deref() + } + + pub fn custom_dismiss_action(&self) -> bool { + self.custom_dismiss_action + } + + pub fn allow_in_car_play(&self) -> bool { + self.allow_in_car_play + } + + pub fn hidden_previews_show_title(&self) -> bool { + self.hidden_previews_show_title + } + + pub fn hidden_previews_show_subtitle(&self) -> bool { + self.hidden_previews_show_subtitle + } +} + +impl ActionTypeBuilder { + pub fn actions(mut self, actions: Vec) -> Self { + self.0.actions = actions; + self + } + + pub fn hidden_previews_body_placeholder( + mut self, + hidden_previews_body_placeholder: impl Into, + ) -> Self { + self.0 + .hidden_previews_body_placeholder + .replace(hidden_previews_body_placeholder.into()); + self + } + + pub fn custom_dismiss_action(mut self, custom_dismiss_action: bool) -> Self { + self.0.custom_dismiss_action = custom_dismiss_action; + self + } + + pub fn allow_in_car_play(mut self, allow_in_car_play: bool) -> Self { + self.0.allow_in_car_play = allow_in_car_play; + self + } + + pub fn hidden_previews_show_title(mut self, hidden_previews_show_title: bool) -> Self { + self.0.hidden_previews_show_title = hidden_previews_show_title; + self + } + + pub fn hidden_previews_show_subtitle(mut self, hidden_previews_show_subtitle: bool) -> Self { + self.0.hidden_previews_show_subtitle = hidden_previews_show_subtitle; + self + } + + pub fn build(self) -> ActionType { + self.0 + } +} + #[cfg(mobile)] #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] @@ -334,6 +420,92 @@ pub struct Action { input_placeholder: Option, } +#[derive(Debug)] +pub struct ActionBuilder(Action); + +impl Action { + pub fn builder(id: impl Into, title: impl Into) -> ActionBuilder { + ActionBuilder(Self { + id: id.into(), + title: title.into(), + requires_authentication: false, + foreground: false, + destructive: false, + input: false, + input_button_title: None, + input_placeholder: None, + }) + } + + pub fn id(&self) -> &str { + &self.id + } + + pub fn title(&self) -> &str { + &self.title + } + + pub fn requires_authentication(&self) -> bool { + self.requires_authentication + } + + pub fn foreground(&self) -> bool { + self.foreground + } + + pub fn destructive(&self) -> bool { + self.destructive + } + + pub fn input(&self) -> bool { + self.input + } + + pub fn input_button_title(&self) -> Option<&str> { + self.input_button_title.as_deref() + } + + pub fn input_placeholder(&self) -> Option<&str> { + self.input_placeholder.as_deref() + } +} + +impl ActionBuilder { + pub fn requires_authentication(mut self, requires_authentication: bool) -> Self { + self.0.requires_authentication = requires_authentication; + self + } + + pub fn foreground(mut self, foreground: bool) -> Self { + self.0.foreground = foreground; + self + } + + pub fn destructive(mut self, destructive: bool) -> Self { + self.0.destructive = destructive; + self + } + + pub fn input(mut self, input: bool) -> Self { + self.0.input = input; + self + } + + pub fn input_button_title(mut self, input_button_title: impl Into) -> Self { + self.0.input_button_title.replace(input_button_title.into()); + self + } + + pub fn input_placeholder(mut self, input_placeholder: impl Into) -> Self { + self.0.input_placeholder.replace(input_placeholder.into()); + self + } + + pub fn build(self) -> Action { + self.0 + } +} + #[cfg(target_os = "android")] pub use android::*;