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
pull/923/head
Lucas Fernandes Nogueira 1 year ago committed by GitHub
parent 61edbbec0a
commit 8dea78ac7d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,6 @@
---
"notification": patch
"notification-js": patch
---
Fixes deserialization and implementation bugs with scheduled notifications on Android.

@ -11,8 +11,8 @@ import process from "process";
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig(async () => { export default defineConfig(async () => {
const host = const host =
process.env.TAURI_PLATFORM === "android" || process.env.TAURI_ENV_PLATFORM === "android" ||
process.env.TAURI_PLATFORM === "ios" process.env.TAURI_ENV_PLATFORM === "ios"
? await internalIpV4() ? await internalIpV4()
: "localhost"; : "localhost";
return { return {

@ -16,4 +16,5 @@
</application> </application>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
</manifest> </manifest>

@ -80,8 +80,6 @@ class Notification {
return null return null
} }
val isScheduled = schedule != null
companion object { companion object {
fun buildNotificationPendingList(notifications: List<Notification>): List<PendingNotification> { fun buildNotificationPendingList(notifications: List<Notification>): List<PendingNotification> {
val pendingNotifications = mutableListOf<PendingNotification>() val pendingNotifications = mutableListOf<PendingNotification>()

@ -268,7 +268,7 @@ class NotificationPlugin(private val activity: Activity): Plugin(activity) {
@PermissionCallback @PermissionCallback
private fun permissionsCallback(invoke: Invoke) { private fun permissionsCallback(invoke: Invoke) {
val permissionsResultJSON = JSObject() val permissionsResultJSON = JSObject()
permissionsResultJSON.put("display", getPermissionState()) permissionsResultJSON.put("permissionState", getPermissionState())
invoke.resolve(permissionsResultJSON) invoke.resolve(permissionsResultJSON)
} }

@ -5,9 +5,9 @@
package app.tauri.notification package app.tauri.notification
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.ClipData.Item
import android.text.format.DateUtils import android.text.format.DateUtils
import com.fasterxml.jackson.annotation.JsonFormat import com.fasterxml.jackson.annotation.JsonFormat
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.core.JsonGenerator
import com.fasterxml.jackson.core.JsonParser import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.core.JsonProcessingException 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.SerializerProvider
import com.fasterxml.jackson.databind.annotation.JsonDeserialize import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.ser.std.StdSerializer import com.fasterxml.jackson.databind.ser.std.StdSerializer
import java.io.IOException import java.io.IOException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -24,11 +25,25 @@ import java.util.Calendar
import java.util.Date import java.util.Date
import java.util.TimeZone import java.util.TimeZone
const val JS_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" const val JS_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
enum class NotificationInterval { 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 { fun getIntervalTime(interval: NotificationInterval, count: Int): Long {
@ -50,9 +65,24 @@ fun getIntervalTime(interval: NotificationInterval, count: Int): Long {
@JsonSerialize(using = NotificationScheduleSerializer::class) @JsonSerialize(using = NotificationScheduleSerializer::class)
sealed class NotificationSchedule { sealed class NotificationSchedule {
// At specific moment of time (with repeating option) // 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() @JsonDeserialize
class Interval(val interval: DateMatch, val allowWhileIdle: Boolean = false): NotificationSchedule() class At: NotificationSchedule() {
class Every(val interval: NotificationInterval, val count: Int = 0, val allowWhileIdle: Boolean = false): 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 { fun isRemovable(): Boolean {
return when (this) { return when (this) {

@ -20,7 +20,7 @@ class NotificationStorage(private val context: Context, private val jsonMapper:
val storage = getStorage(NOTIFICATION_STORE_ID) val storage = getStorage(NOTIFICATION_STORE_ID)
val editor = storage.edit() val editor = storage.edit()
for (request in localNotifications) { for (request in localNotifications) {
if (request.isScheduled) { if (request.schedule != null) {
val key: String = request.id.toString() val key: String = request.id.toString()
editor.putString(key, request.sourceJson.toString()) editor.putString(key, request.sourceJson.toString())
} }

@ -212,7 +212,7 @@ class TauriNotificationManager(
createActionIntents(notification, mBuilder) createActionIntents(notification, mBuilder)
// notificationId is a unique int for each notification that you must define // notificationId is a unique int for each notification that you must define
val buildNotification = mBuilder.build() val buildNotification = mBuilder.build()
if (notification.isScheduled) { if (notification.schedule != null) {
triggerScheduledNotification(buildNotification, notification) triggerScheduledNotification(buildNotification, notification)
} else { } else {
notificationManager.notify(notification.id, buildNotification) notificationManager.notify(notification.id, buildNotification)
@ -473,7 +473,7 @@ class TimedNotificationPublisher : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
val notificationManager = val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as 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( intent.getParcelableExtra(
NOTIFICATION_KEY, NOTIFICATION_KEY,
android.app.Notification::class.java android.app.Notification::class.java

@ -163,49 +163,57 @@ enum ScheduleEvery {
Second = "second", Second = "second",
} }
type ScheduleData = class Schedule {
at:
| { | {
at: {
date: Date; date: Date;
repeating: boolean; repeating: boolean;
allowWhileIdle: boolean; allowWhileIdle: boolean;
};
} }
| undefined;
interval:
| { | {
interval: {
interval: ScheduleInterval; interval: ScheduleInterval;
allowWhileIdle: boolean; allowWhileIdle: boolean;
};
} }
| undefined;
every:
| { | {
every: {
interval: ScheduleEvery; interval: ScheduleEvery;
count: number; count: number;
allowWhileIdle: boolean; allowWhileIdle: boolean;
};
};
class Schedule {
schedule: ScheduleData;
private constructor(schedule: ScheduleData) {
this.schedule = schedule;
}
toJSON(): string {
return JSON.stringify(this.schedule);
} }
| undefined;
static at(date: Date, repeating = false, allowWhileIdle = false) { static at(date: Date, repeating = false, allowWhileIdle = false): Schedule {
return new Schedule({ at: { date, repeating, allowWhileIdle } }); return {
at: { date, repeating, allowWhileIdle },
interval: undefined,
every: undefined,
};
} }
static interval(interval: ScheduleInterval, allowWhileIdle = false) { static interval(
return new Schedule({ interval: { interval, allowWhileIdle } }); interval: ScheduleInterval,
allowWhileIdle = false,
): Schedule {
return {
at: undefined,
interval: { interval, allowWhileIdle },
every: undefined,
};
} }
static every(kind: ScheduleEvery, count: number, allowWhileIdle = false) { static every(
return new Schedule({ every: { interval: kind, count, allowWhileIdle } }); kind: ScheduleEvery,
count: number,
allowWhileIdle = false,
): Schedule {
return {
at: undefined,
interval: undefined,
every: { interval: kind, count, allowWhileIdle },
};
} }
} }

@ -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__})}

@ -51,14 +51,14 @@ impl Display for ScheduleEvery {
f, f,
"{}", "{}",
match self { match self {
Self::Year => "Year", Self::Year => "year",
Self::Month => "Month", Self::Month => "month",
Self::TwoWeeks => "TwoWeeks", Self::TwoWeeks => "twoWeeks",
Self::Week => "Week", Self::Week => "week",
Self::Day => "Day", Self::Day => "day",
Self::Hour => "Hour", Self::Hour => "hour",
Self::Minute => "Minute", Self::Minute => "minute",
Self::Second => "Second", Self::Second => "second",
} }
) )
} }
@ -96,6 +96,7 @@ impl<'de> Deserialize<'de> for ScheduleEvery {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub enum Schedule { pub enum Schedule {
#[serde(rename_all = "camelCase")]
At { At {
#[serde( #[serde(
serialize_with = "iso8601::serialize", serialize_with = "iso8601::serialize",
@ -107,11 +108,13 @@ pub enum Schedule {
#[serde(default)] #[serde(default)]
allow_while_idle: bool, allow_while_idle: bool,
}, },
#[serde(rename_all = "camelCase")]
Interval { Interval {
interval: ScheduleInterval, interval: ScheduleInterval,
#[serde(default)] #[serde(default)]
allow_while_idle: bool, allow_while_idle: bool,
}, },
#[serde(rename_all = "camelCase")]
Every { Every {
interval: ScheduleEvery, interval: ScheduleEvery,
count: u8, count: u8,

Loading…
Cancel
Save