From 8dea78ac7dcb502159e66bad464094696aa257d4 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Wed, 24 Jan 2024 13:02:11 -0300 Subject: [PATCH] fix(notification): scheduled notifications not working (#909) * fix(notification): scheduled notifications not working * do not use toJSON since it doesnt work on isolation pattern * fmt --- .changes/fix-scheduled-notification.md | 6 ++ examples/api/vite.config.js | 4 +- .../android/src/main/AndroidManifest.xml | 7 +- .../android/src/main/java/Notification.kt | 2 - .../src/main/java/NotificationPlugin.kt | 2 +- .../src/main/java/NotificationSchedule.kt | 42 ++++++++++-- .../src/main/java/NotificationStorage.kt | 2 +- .../src/main/java/TauriNotificationManager.kt | 4 +- plugins/notification/guest-js/index.ts | 68 +++++++++++-------- plugins/notification/src/api-iife.js | 2 +- plugins/notification/src/models.rs | 19 +++--- 11 files changed, 102 insertions(+), 56 deletions(-) create mode 100644 .changes/fix-scheduled-notification.md diff --git a/.changes/fix-scheduled-notification.md b/.changes/fix-scheduled-notification.md new file mode 100644 index 00000000..0fb5afde --- /dev/null +++ b/.changes/fix-scheduled-notification.md @@ -0,0 +1,6 @@ +--- +"notification": patch +"notification-js": patch +--- + +Fixes deserialization and implementation bugs with scheduled notifications on Android. diff --git a/examples/api/vite.config.js b/examples/api/vite.config.js index df8c64f2..e86960b4 100644 --- a/examples/api/vite.config.js +++ b/examples/api/vite.config.js @@ -11,8 +11,8 @@ import process from "process"; // https://vitejs.dev/config/ export default defineConfig(async () => { const host = - process.env.TAURI_PLATFORM === "android" || - process.env.TAURI_PLATFORM === "ios" + process.env.TAURI_ENV_PLATFORM === "android" || + process.env.TAURI_ENV_PLATFORM === "ios" ? await internalIpV4() : "localhost"; return { diff --git a/plugins/notification/android/src/main/AndroidManifest.xml b/plugins/notification/android/src/main/AndroidManifest.xml index 608de0c7..c49808ee 100644 --- a/plugins/notification/android/src/main/AndroidManifest.xml +++ b/plugins/notification/android/src/main/AndroidManifest.xml @@ -14,6 +14,7 @@ - - - + + + + \ No newline at end of file diff --git a/plugins/notification/android/src/main/java/Notification.kt b/plugins/notification/android/src/main/java/Notification.kt index 60e40675..bf10f3dc 100644 --- a/plugins/notification/android/src/main/java/Notification.kt +++ b/plugins/notification/android/src/main/java/Notification.kt @@ -80,8 +80,6 @@ class Notification { return null } - val isScheduled = schedule != null - companion object { fun buildNotificationPendingList(notifications: List): List { val pendingNotifications = mutableListOf() diff --git a/plugins/notification/android/src/main/java/NotificationPlugin.kt b/plugins/notification/android/src/main/java/NotificationPlugin.kt index 4b833dbc..3ead3152 100644 --- a/plugins/notification/android/src/main/java/NotificationPlugin.kt +++ b/plugins/notification/android/src/main/java/NotificationPlugin.kt @@ -268,7 +268,7 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) { @PermissionCallback private fun permissionsCallback(invoke: Invoke) { val permissionsResultJSON = JSObject() - permissionsResultJSON.put("display", getPermissionState()) + permissionsResultJSON.put("permissionState", getPermissionState()) invoke.resolve(permissionsResultJSON) } diff --git a/plugins/notification/android/src/main/java/NotificationSchedule.kt b/plugins/notification/android/src/main/java/NotificationSchedule.kt index 84bad6fa..459461b8 100644 --- a/plugins/notification/android/src/main/java/NotificationSchedule.kt +++ b/plugins/notification/android/src/main/java/NotificationSchedule.kt @@ -5,9 +5,9 @@ package app.tauri.notification import android.annotation.SuppressLint -import android.content.ClipData.Item import android.text.format.DateUtils import com.fasterxml.jackson.annotation.JsonFormat +import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.core.JsonProcessingException @@ -17,6 +17,7 @@ import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.SerializerProvider import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.annotation.JsonSerialize +import com.fasterxml.jackson.databind.deser.std.StdDeserializer import com.fasterxml.jackson.databind.ser.std.StdSerializer import java.io.IOException import java.text.SimpleDateFormat @@ -24,11 +25,25 @@ import java.util.Calendar import java.util.Date import java.util.TimeZone - const val JS_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" enum class NotificationInterval { - Year, Month, TwoWeeks, Week, Day, Hour, Minute, Second + @JsonProperty("year") + Year, + @JsonProperty("month") + Month, + @JsonProperty("twoWeeks") + TwoWeeks, + @JsonProperty("week") + Week, + @JsonProperty("day") + Day, + @JsonProperty("hour") + Hour, + @JsonProperty("minute") + Minute, + @JsonProperty("second") + Second } fun getIntervalTime(interval: NotificationInterval, count: Int): Long { @@ -50,9 +65,24 @@ fun getIntervalTime(interval: NotificationInterval, count: Int): Long { @JsonSerialize(using = NotificationScheduleSerializer::class) sealed class NotificationSchedule { // At specific moment of time (with repeating option) - class At(@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = JS_DATE_FORMAT) var date: Date, val repeating: Boolean = false, val allowWhileIdle: Boolean = false): NotificationSchedule() - class Interval(val interval: DateMatch, val allowWhileIdle: Boolean = false): NotificationSchedule() - class Every(val interval: NotificationInterval, val count: Int = 0, val allowWhileIdle: Boolean = false): NotificationSchedule() + @JsonDeserialize + class At: NotificationSchedule() { + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = JS_DATE_FORMAT) + lateinit var date: Date + var repeating: Boolean = false + var allowWhileIdle: Boolean = false + } + @JsonDeserialize + class Interval: NotificationSchedule() { + lateinit var interval: DateMatch + var allowWhileIdle: Boolean = false + } + @JsonDeserialize + class Every: NotificationSchedule() { + lateinit var interval: NotificationInterval + var count: Int = 0 + var allowWhileIdle: Boolean = false + } fun isRemovable(): Boolean { return when (this) { diff --git a/plugins/notification/android/src/main/java/NotificationStorage.kt b/plugins/notification/android/src/main/java/NotificationStorage.kt index 273b37c8..bceb985d 100644 --- a/plugins/notification/android/src/main/java/NotificationStorage.kt +++ b/plugins/notification/android/src/main/java/NotificationStorage.kt @@ -20,7 +20,7 @@ class NotificationStorage(private val context: Context, private val jsonMapper: val storage = getStorage(NOTIFICATION_STORE_ID) val editor = storage.edit() for (request in localNotifications) { - if (request.isScheduled) { + if (request.schedule != null) { val key: String = request.id.toString() editor.putString(key, request.sourceJson.toString()) } diff --git a/plugins/notification/android/src/main/java/TauriNotificationManager.kt b/plugins/notification/android/src/main/java/TauriNotificationManager.kt index e653fab7..a8912739 100644 --- a/plugins/notification/android/src/main/java/TauriNotificationManager.kt +++ b/plugins/notification/android/src/main/java/TauriNotificationManager.kt @@ -212,7 +212,7 @@ class TauriNotificationManager( createActionIntents(notification, mBuilder) // notificationId is a unique int for each notification that you must define val buildNotification = mBuilder.build() - if (notification.isScheduled) { + if (notification.schedule != null) { triggerScheduledNotification(buildNotification, notification) } else { notificationManager.notify(notification.id, buildNotification) @@ -473,7 +473,7 @@ class TimedNotificationPublisher : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - val notification = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val notification = if (SDK_INT >= Build.VERSION_CODES.TIRAMISU) { intent.getParcelableExtra( NOTIFICATION_KEY, android.app.Notification::class.java diff --git a/plugins/notification/guest-js/index.ts b/plugins/notification/guest-js/index.ts index d121c5d1..b7bac7d7 100644 --- a/plugins/notification/guest-js/index.ts +++ b/plugins/notification/guest-js/index.ts @@ -163,49 +163,57 @@ enum ScheduleEvery { Second = "second", } -type ScheduleData = - | { - at: { +class Schedule { + at: + | { date: Date; repeating: boolean; allowWhileIdle: boolean; - }; - } - | { - interval: { + } + | undefined; + interval: + | { interval: ScheduleInterval; allowWhileIdle: boolean; - }; - } - | { - every: { + } + | undefined; + every: + | { interval: ScheduleEvery; count: number; allowWhileIdle: boolean; - }; + } + | undefined; + + static at(date: Date, repeating = false, allowWhileIdle = false): Schedule { + return { + at: { date, repeating, allowWhileIdle }, + interval: undefined, + every: undefined, }; - -class Schedule { - schedule: ScheduleData; - - private constructor(schedule: ScheduleData) { - this.schedule = schedule; - } - - toJSON(): string { - return JSON.stringify(this.schedule); - } - - static at(date: Date, repeating = false, allowWhileIdle = false) { - return new Schedule({ at: { date, repeating, allowWhileIdle } }); } - static interval(interval: ScheduleInterval, allowWhileIdle = false) { - return new Schedule({ interval: { interval, allowWhileIdle } }); + static interval( + interval: ScheduleInterval, + allowWhileIdle = false, + ): Schedule { + return { + at: undefined, + interval: { interval, allowWhileIdle }, + every: undefined, + }; } - static every(kind: ScheduleEvery, count: number, allowWhileIdle = false) { - return new Schedule({ every: { interval: kind, count, allowWhileIdle } }); + static every( + kind: ScheduleEvery, + count: number, + allowWhileIdle = false, + ): Schedule { + return { + at: undefined, + interval: undefined, + every: { interval: kind, count, allowWhileIdle }, + }; } } diff --git a/plugins/notification/src/api-iife.js b/plugins/notification/src/api-iife.js index 6cc89550..3d8c020a 100644 --- a/plugins/notification/src/api-iife.js +++ b/plugins/notification/src/api-iife.js @@ -1 +1 @@ -if("__TAURI__"in window){var __TAURI_PLUGIN_NOTIFICATION__=function(n){"use strict";function e(n,e,i,t){if("a"===i&&!t)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof e?n!==e||!t:!e.has(n))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===i?t:"a"===i?t.call(n):t?t.value:e.get(n)}var i,t,o,r;"function"==typeof SuppressedError&&SuppressedError;class a{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,i.set(this,(()=>{})),this.id=function(n,e=!1){return window.__TAURI_INTERNALS__.transformCallback(n,e)}((n=>{e(this,i,"f").call(this,n)}))}set onmessage(n){!function(n,e,i,t,o){if("m"===t)throw new TypeError("Private method is not writable");if("a"===t&&!o)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof e?n!==e||!o:!e.has(n))throw new TypeError("Cannot write private member to an object whose class did not declare it");"a"===t?o.call(n,i):o?o.value=i:e.set(n,i)}(this,i,n,"f")}get onmessage(){return e(this,i,"f")}toJSON(){return`__CHANNEL__:${this.id}`}}i=new WeakMap;class c{constructor(n,e,i){this.plugin=n,this.event=e,this.channelId=i}async unregister(){return u(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}}async function s(n,e,i){const t=new a;return t.onmessage=i,u(`plugin:${n}|register_listener`,{event:e,handler:t}).then((()=>new c(n,e,t.id)))}async function u(n,e={},i){return window.__TAURI_INTERNALS__.invoke(n,e,i)}n.ScheduleEvery=void 0,(t=n.ScheduleEvery||(n.ScheduleEvery={})).Year="year",t.Month="month",t.TwoWeeks="twoWeeks",t.Week="week",t.Day="day",t.Hour="hour",t.Minute="minute",t.Second="second";class l{constructor(n){this.schedule=n}toJSON(){return JSON.stringify(this.schedule)}static at(n,e=!1,i=!1){return new l({at:{date:n,repeating:e,allowWhileIdle:i}})}static interval(n,e=!1){return new l({interval:{interval:n,allowWhileIdle:e}})}static every(n,e,i=!1){return new l({every:{interval:n,count:e,allowWhileIdle:i}})}}return n.Importance=void 0,(o=n.Importance||(n.Importance={}))[o.None=0]="None",o[o.Min=1]="Min",o[o.Low=2]="Low",o[o.Default=3]="Default",o[o.High=4]="High",n.Visibility=void 0,(r=n.Visibility||(n.Visibility={}))[r.Secret=-1]="Secret",r[r.Private=0]="Private",r[r.Public=1]="Public",n.Schedule=l,n.active=async function(){return u("plugin:notification|get_active")},n.cancel=async function(n){return u("plugin:notification|cancel",{notifications:n})},n.cancelAll=async function(){return u("plugin:notification|cancel")},n.channels=async function(){return u("plugin:notification|listChannels")},n.createChannel=async function(n){return u("plugin:notification|create_channel",{...n})},n.isPermissionGranted=async function(){return"default"!==window.Notification.permission?Promise.resolve("granted"===window.Notification.permission):u("plugin:notification|is_permission_granted")},n.onAction=async function(n){return s("notification","actionPerformed",n)},n.onNotificationReceived=async function(n){return s("notification","notification",n)},n.pending=async function(){return u("plugin:notification|get_pending")},n.registerActionTypes=async function(n){return u("plugin:notification|register_action_types",{types:n})},n.removeActive=async function(n){return u("plugin:notification|remove_active",{notifications:n})},n.removeAllActive=async function(){return u("plugin:notification|remove_active")},n.removeChannel=async function(n){return u("plugin:notification|delete_channel",{id:n})},n.requestPermission=async function(){return window.Notification.requestPermission()},n.sendNotification=function(n){"string"==typeof n?new window.Notification(n):new window.Notification(n.title,n)},n}({});Object.defineProperty(window.__TAURI__,"notification",{value:__TAURI_PLUGIN_NOTIFICATION__})} +if("__TAURI__"in window){var __TAURI_PLUGIN_NOTIFICATION__=function(n){"use strict";function e(n,e,i,t){if("a"===i&&!t)throw new TypeError("Private accessor was defined without a getter");if("function"==typeof e?n!==e||!t:!e.has(n))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===i?t:"a"===i?t.call(n):t?t.value:e.get(n)}var i,t,o,r;"function"==typeof SuppressedError&&SuppressedError;class a{constructor(){this.__TAURI_CHANNEL_MARKER__=!0,i.set(this,(()=>{})),this.id=function(n,e=!1){return window.__TAURI_INTERNALS__.transformCallback(n,e)}((n=>{e(this,i,"f").call(this,n)}))}set onmessage(n){!function(n,e,i,t,o){if("m"===t)throw new TypeError("Private method is not writable");if("a"===t&&!o)throw new TypeError("Private accessor was defined without a setter");if("function"==typeof e?n!==e||!o:!e.has(n))throw new TypeError("Cannot write private member to an object whose class did not declare it");"a"===t?o.call(n,i):o?o.value=i:e.set(n,i)}(this,i,n,"f")}get onmessage(){return e(this,i,"f")}toJSON(){return`__CHANNEL__:${this.id}`}}i=new WeakMap;class c{constructor(n,e,i){this.plugin=n,this.event=e,this.channelId=i}async unregister(){return u(`plugin:${this.plugin}|remove_listener`,{event:this.event,channelId:this.channelId})}}async function s(n,e,i){const t=new a;return t.onmessage=i,u(`plugin:${n}|register_listener`,{event:e,handler:t}).then((()=>new c(n,e,t.id)))}async function u(n,e={},i){return window.__TAURI_INTERNALS__.invoke(n,e,i)}n.ScheduleEvery=void 0,(t=n.ScheduleEvery||(n.ScheduleEvery={})).Year="year",t.Month="month",t.TwoWeeks="twoWeeks",t.Week="week",t.Day="day",t.Hour="hour",t.Minute="minute",t.Second="second";return n.Importance=void 0,(o=n.Importance||(n.Importance={}))[o.None=0]="None",o[o.Min=1]="Min",o[o.Low=2]="Low",o[o.Default=3]="Default",o[o.High=4]="High",n.Visibility=void 0,(r=n.Visibility||(n.Visibility={}))[r.Secret=-1]="Secret",r[r.Private=0]="Private",r[r.Public=1]="Public",n.Schedule=class{static at(n,e=!1,i=!1){return{at:{date:n,repeating:e,allowWhileIdle:i},interval:void 0,every:void 0}}static interval(n,e=!1){return{at:void 0,interval:{interval:n,allowWhileIdle:e},every:void 0}}static every(n,e,i=!1){return{at:void 0,interval:void 0,every:{interval:n,count:e,allowWhileIdle:i}}}},n.active=async function(){return u("plugin:notification|get_active")},n.cancel=async function(n){return u("plugin:notification|cancel",{notifications:n})},n.cancelAll=async function(){return u("plugin:notification|cancel")},n.channels=async function(){return u("plugin:notification|listChannels")},n.createChannel=async function(n){return u("plugin:notification|create_channel",{...n})},n.isPermissionGranted=async function(){return"default"!==window.Notification.permission?Promise.resolve("granted"===window.Notification.permission):u("plugin:notification|is_permission_granted")},n.onAction=async function(n){return s("notification","actionPerformed",n)},n.onNotificationReceived=async function(n){return s("notification","notification",n)},n.pending=async function(){return u("plugin:notification|get_pending")},n.registerActionTypes=async function(n){return u("plugin:notification|register_action_types",{types:n})},n.removeActive=async function(n){return u("plugin:notification|remove_active",{notifications:n})},n.removeAllActive=async function(){return u("plugin:notification|remove_active")},n.removeChannel=async function(n){return u("plugin:notification|delete_channel",{id:n})},n.requestPermission=async function(){return window.Notification.requestPermission()},n.sendNotification=function(n){"string"==typeof n?new window.Notification(n):new window.Notification(n.title,n)},n}({});Object.defineProperty(window.__TAURI__,"notification",{value:__TAURI_PLUGIN_NOTIFICATION__})} diff --git a/plugins/notification/src/models.rs b/plugins/notification/src/models.rs index 0db617ed..aa290dd3 100644 --- a/plugins/notification/src/models.rs +++ b/plugins/notification/src/models.rs @@ -51,14 +51,14 @@ impl Display for ScheduleEvery { f, "{}", match self { - Self::Year => "Year", - Self::Month => "Month", - Self::TwoWeeks => "TwoWeeks", - Self::Week => "Week", - Self::Day => "Day", - Self::Hour => "Hour", - Self::Minute => "Minute", - Self::Second => "Second", + Self::Year => "year", + Self::Month => "month", + Self::TwoWeeks => "twoWeeks", + Self::Week => "week", + Self::Day => "day", + Self::Hour => "hour", + Self::Minute => "minute", + Self::Second => "second", } ) } @@ -96,6 +96,7 @@ impl<'de> Deserialize<'de> for ScheduleEvery { #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum Schedule { + #[serde(rename_all = "camelCase")] At { #[serde( serialize_with = "iso8601::serialize", @@ -107,11 +108,13 @@ pub enum Schedule { #[serde(default)] allow_while_idle: bool, }, + #[serde(rename_all = "camelCase")] Interval { interval: ScheduleInterval, #[serde(default)] allow_while_idle: bool, }, + #[serde(rename_all = "camelCase")] Every { interval: ScheduleEvery, count: u8,