Skip to main content

tauri_plugin_notifications/
models.rs

1use std::{collections::HashMap, fmt::Display};
2
3use serde::{de::Error as DeError, Deserialize, Deserializer, Serialize, Serializer};
4use tauri::plugin::PermissionState;
5
6use url::Url;
7
8#[derive(Debug, Deserialize)]
9#[serde(rename_all = "camelCase")]
10pub struct PermissionResponse {
11    pub permission_state: PermissionState,
12}
13
14#[cfg(feature = "push-notifications")]
15#[derive(Debug, Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub struct PushNotificationResponse {
18    pub device_token: String,
19}
20
21#[derive(Debug, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub struct Attachment {
24    id: String,
25    url: Url,
26}
27
28impl Attachment {
29    pub fn new(id: impl Into<String>, url: Url) -> Self {
30        Self { id: id.into(), url }
31    }
32}
33
34#[derive(Debug, Default, Serialize, Deserialize)]
35#[serde(rename_all = "camelCase")]
36pub struct ScheduleInterval {
37    pub year: Option<u8>,
38    pub month: Option<u8>,
39    pub day: Option<u8>,
40    pub weekday: Option<u8>,
41    pub hour: Option<u8>,
42    pub minute: Option<u8>,
43    pub second: Option<u8>,
44}
45
46#[derive(Debug)]
47pub enum ScheduleEvery {
48    Year,
49    Month,
50    TwoWeeks,
51    Week,
52    Day,
53    Hour,
54    Minute,
55    Second,
56}
57
58impl Display for ScheduleEvery {
59    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60        write!(
61            f,
62            "{}",
63            match self {
64                Self::Year => "year",
65                Self::Month => "month",
66                Self::TwoWeeks => "twoWeeks",
67                Self::Week => "week",
68                Self::Day => "day",
69                Self::Hour => "hour",
70                Self::Minute => "minute",
71                Self::Second => "second",
72            }
73        )
74    }
75}
76
77impl Serialize for ScheduleEvery {
78    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
79    where
80        S: Serializer,
81    {
82        serializer.serialize_str(self.to_string().as_ref())
83    }
84}
85
86impl<'de> Deserialize<'de> for ScheduleEvery {
87    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
88    where
89        D: Deserializer<'de>,
90    {
91        let s = String::deserialize(deserializer)?;
92        match s.to_lowercase().as_str() {
93            "year" => Ok(Self::Year),
94            "month" => Ok(Self::Month),
95            "twoweeks" => Ok(Self::TwoWeeks),
96            "week" => Ok(Self::Week),
97            "day" => Ok(Self::Day),
98            "hour" => Ok(Self::Hour),
99            "minute" => Ok(Self::Minute),
100            "second" => Ok(Self::Second),
101            _ => Err(DeError::custom(format!("unknown every kind '{s}'"))),
102        }
103    }
104}
105
106#[derive(Debug, Serialize, Deserialize)]
107#[serde(rename_all = "camelCase")]
108pub enum Schedule {
109    #[serde(rename_all = "camelCase")]
110    At {
111        #[serde(
112            serialize_with = "iso8601::serialize",
113            deserialize_with = "time::serde::iso8601::deserialize"
114        )]
115        date: time::OffsetDateTime,
116        #[serde(default)]
117        repeating: bool,
118        #[serde(default)]
119        allow_while_idle: bool,
120    },
121    #[serde(rename_all = "camelCase")]
122    Interval {
123        interval: ScheduleInterval,
124        #[serde(default)]
125        allow_while_idle: bool,
126    },
127    #[serde(rename_all = "camelCase")]
128    Every {
129        interval: ScheduleEvery,
130        count: u8,
131        #[serde(default)]
132        allow_while_idle: bool,
133    },
134}
135
136// custom ISO-8601 serialization that does not use 6 digits for years.
137mod iso8601 {
138    use serde::{ser::Error as _, Serialize, Serializer};
139    use time::{
140        format_description::well_known::iso8601::{Config, EncodedConfig},
141        format_description::well_known::Iso8601,
142        OffsetDateTime,
143    };
144
145    const SERDE_CONFIG: EncodedConfig = Config::DEFAULT.encode();
146
147    pub fn serialize<S: Serializer>(
148        datetime: &OffsetDateTime,
149        serializer: S,
150    ) -> Result<S::Ok, S::Error> {
151        datetime
152            .format(&Iso8601::<SERDE_CONFIG>)
153            .map_err(S::Error::custom)?
154            .serialize(serializer)
155    }
156}
157
158// Each bool is an independent flag in the JS wire format; grouping them would change the JSON shape.
159#[allow(clippy::struct_excessive_bools)]
160#[derive(Debug, Serialize, Deserialize)]
161#[serde(rename_all = "camelCase")]
162pub struct NotificationData {
163    #[serde(default = "default_id")]
164    pub(crate) id: i32,
165    pub(crate) channel_id: Option<String>,
166    pub(crate) title: Option<String>,
167    pub(crate) body: Option<String>,
168    pub(crate) schedule: Option<Schedule>,
169    pub(crate) large_body: Option<String>,
170    pub(crate) summary: Option<String>,
171    pub(crate) action_type_id: Option<String>,
172    pub(crate) group: Option<String>,
173    #[serde(default)]
174    pub(crate) group_summary: bool,
175    pub(crate) sound: Option<String>,
176    #[serde(default)]
177    pub(crate) inbox_lines: Vec<String>,
178    pub(crate) icon: Option<String>,
179    pub(crate) large_icon: Option<String>,
180    pub(crate) icon_color: Option<String>,
181    #[serde(default)]
182    pub(crate) attachments: Vec<Attachment>,
183    #[serde(default)]
184    pub(crate) extra: HashMap<String, serde_json::Value>,
185    #[serde(default)]
186    pub(crate) ongoing: bool,
187    #[serde(default)]
188    pub(crate) auto_cancel: bool,
189    #[serde(default)]
190    pub(crate) silent: bool,
191}
192
193fn default_id() -> i32 {
194    rand::random()
195}
196
197impl Default for NotificationData {
198    fn default() -> Self {
199        Self {
200            id: default_id(),
201            channel_id: None,
202            title: None,
203            body: None,
204            schedule: None,
205            large_body: None,
206            summary: None,
207            action_type_id: None,
208            group: None,
209            group_summary: false,
210            sound: None,
211            inbox_lines: Vec::new(),
212            icon: None,
213            large_icon: None,
214            icon_color: None,
215            attachments: Vec::new(),
216            extra: HashMap::default(),
217            ongoing: false,
218            auto_cancel: false,
219            silent: false,
220        }
221    }
222}
223
224#[derive(Debug, Deserialize, Serialize)]
225#[serde(rename_all = "camelCase")]
226pub struct PendingNotification {
227    id: i32,
228    title: Option<String>,
229    body: Option<String>,
230    schedule: Schedule,
231}
232
233impl PendingNotification {
234    #[must_use]
235    pub const fn id(&self) -> i32 {
236        self.id
237    }
238
239    #[must_use]
240    pub fn title(&self) -> Option<&str> {
241        self.title.as_deref()
242    }
243
244    #[must_use]
245    pub fn body(&self) -> Option<&str> {
246        self.body.as_deref()
247    }
248
249    #[must_use]
250    pub const fn schedule(&self) -> &Schedule {
251        &self.schedule
252    }
253}
254
255#[derive(Debug, Deserialize, Serialize)]
256#[serde(rename_all = "camelCase")]
257pub struct ActiveNotification {
258    id: i32,
259    tag: Option<String>,
260    title: Option<String>,
261    body: Option<String>,
262    group: Option<String>,
263    #[serde(default)]
264    group_summary: bool,
265    #[serde(default)]
266    data: HashMap<String, String>,
267    #[serde(default)]
268    extra: HashMap<String, serde_json::Value>,
269    #[serde(default)]
270    attachments: Vec<Attachment>,
271    action_type_id: Option<String>,
272    schedule: Option<Schedule>,
273    sound: Option<String>,
274}
275
276impl ActiveNotification {
277    #[must_use]
278    pub const fn id(&self) -> i32 {
279        self.id
280    }
281
282    #[must_use]
283    pub fn tag(&self) -> Option<&str> {
284        self.tag.as_deref()
285    }
286
287    #[must_use]
288    pub fn title(&self) -> Option<&str> {
289        self.title.as_deref()
290    }
291
292    #[must_use]
293    pub fn body(&self) -> Option<&str> {
294        self.body.as_deref()
295    }
296
297    #[must_use]
298    pub fn group(&self) -> Option<&str> {
299        self.group.as_deref()
300    }
301
302    #[must_use]
303    pub const fn group_summary(&self) -> bool {
304        self.group_summary
305    }
306
307    #[must_use]
308    pub const fn data(&self) -> &HashMap<String, String> {
309        &self.data
310    }
311
312    #[must_use]
313    pub const fn extra(&self) -> &HashMap<String, serde_json::Value> {
314        &self.extra
315    }
316
317    #[must_use]
318    pub fn attachments(&self) -> &[Attachment] {
319        &self.attachments
320    }
321
322    #[must_use]
323    pub fn action_type_id(&self) -> Option<&str> {
324        self.action_type_id.as_deref()
325    }
326
327    #[must_use]
328    pub const fn schedule(&self) -> Option<&Schedule> {
329        self.schedule.as_ref()
330    }
331
332    #[must_use]
333    pub fn sound(&self) -> Option<&str> {
334        self.sound.as_deref()
335    }
336}
337
338// Each bool is an independent UNNotificationCategory option; grouping would change the JSON shape.
339#[allow(clippy::struct_excessive_bools)]
340#[derive(Debug, Serialize, Deserialize)]
341#[serde(rename_all = "camelCase")]
342pub struct ActionType {
343    id: String,
344    actions: Vec<Action>,
345    hidden_previews_body_placeholder: Option<String>,
346    #[serde(default)]
347    custom_dismiss_action: bool,
348    #[serde(default)]
349    allow_in_car_play: bool,
350    #[serde(default)]
351    hidden_previews_show_title: bool,
352    #[serde(default)]
353    hidden_previews_show_subtitle: bool,
354}
355
356// Each bool is an independent UNNotificationAction option; grouping would change the JSON shape.
357#[allow(clippy::struct_excessive_bools)]
358#[derive(Debug, Serialize, Deserialize)]
359#[serde(rename_all = "camelCase")]
360pub struct Action {
361    id: String,
362    title: String,
363    #[serde(default)]
364    requires_authentication: bool,
365    #[serde(default)]
366    foreground: bool,
367    #[serde(default)]
368    destructive: bool,
369    #[serde(default)]
370    input: bool,
371    input_button_title: Option<String>,
372    input_placeholder: Option<String>,
373}
374
375pub use android::*;
376
377mod android {
378    use serde::{Deserialize, Serialize};
379    use serde_repr::{Deserialize_repr, Serialize_repr};
380
381    #[derive(Debug, Default, Clone, Copy, Serialize_repr, Deserialize_repr)]
382    #[repr(u8)]
383    pub enum Importance {
384        None = 0,
385        Min = 1,
386        Low = 2,
387        #[default]
388        Default = 3,
389        High = 4,
390    }
391
392    #[derive(Debug, Clone, Copy, Serialize_repr, Deserialize_repr)]
393    #[repr(i8)]
394    pub enum Visibility {
395        Secret = -1,
396        Private = 0,
397        Public = 1,
398    }
399
400    #[derive(Debug, Serialize, Deserialize)]
401    #[serde(rename_all = "camelCase")]
402    pub struct Channel {
403        id: String,
404        name: String,
405        description: Option<String>,
406        sound: Option<String>,
407        lights: Option<bool>,
408        light_color: Option<String>,
409        vibration: Option<bool>,
410        importance: Option<Importance>,
411        visibility: Option<Visibility>,
412    }
413
414    #[derive(Debug)]
415    pub struct ChannelBuilder(Channel);
416
417    impl Channel {
418        pub fn builder(id: impl Into<String>, name: impl Into<String>) -> ChannelBuilder {
419            ChannelBuilder(Self {
420                id: id.into(),
421                name: name.into(),
422                description: None,
423                sound: None,
424                lights: Some(false),
425                light_color: None,
426                vibration: Some(false),
427                importance: None,
428                visibility: None,
429            })
430        }
431
432        #[must_use]
433        pub fn id(&self) -> &str {
434            &self.id
435        }
436
437        #[must_use]
438        pub fn name(&self) -> &str {
439            &self.name
440        }
441
442        #[must_use]
443        pub fn description(&self) -> Option<&str> {
444            self.description.as_deref()
445        }
446
447        #[must_use]
448        pub fn sound(&self) -> Option<&str> {
449            self.sound.as_deref()
450        }
451
452        #[must_use]
453        pub fn lights(&self) -> bool {
454            self.lights.unwrap_or(false)
455        }
456
457        #[must_use]
458        pub fn light_color(&self) -> Option<&str> {
459            self.light_color.as_deref()
460        }
461
462        #[must_use]
463        pub fn vibration(&self) -> bool {
464            self.vibration.unwrap_or(false)
465        }
466
467        #[must_use]
468        pub fn importance(&self) -> Importance {
469            self.importance.unwrap_or_default()
470        }
471
472        #[must_use]
473        pub const fn visibility(&self) -> Option<Visibility> {
474            self.visibility
475        }
476    }
477
478    impl ChannelBuilder {
479        #[must_use]
480        pub fn description(mut self, description: impl Into<String>) -> Self {
481            self.0.description.replace(description.into());
482            self
483        }
484
485        #[must_use]
486        pub fn sound(mut self, sound: impl Into<String>) -> Self {
487            self.0.sound.replace(sound.into());
488            self
489        }
490
491        #[must_use]
492        pub const fn lights(mut self, lights: bool) -> Self {
493            self.0.lights = Some(lights);
494            self
495        }
496
497        #[must_use]
498        pub fn light_color(mut self, color: impl Into<String>) -> Self {
499            self.0.light_color.replace(color.into());
500            self
501        }
502
503        #[must_use]
504        pub const fn vibration(mut self, vibration: bool) -> Self {
505            self.0.vibration = Some(vibration);
506            self
507        }
508
509        #[must_use]
510        pub const fn importance(mut self, importance: Importance) -> Self {
511            self.0.importance = Some(importance);
512            self
513        }
514
515        #[must_use]
516        pub fn visibility(mut self, visibility: Visibility) -> Self {
517            self.0.visibility.replace(visibility);
518            self
519        }
520
521        #[must_use]
522        pub fn build(self) -> Channel {
523            self.0
524        }
525    }
526}
527
528#[cfg(test)]
529mod tests {
530    use super::*;
531
532    #[test]
533    fn test_attachment_creation() {
534        let url = Url::parse("https://example.com/image.png").expect("Failed to parse URL");
535        let attachment = Attachment::new("test_id", url.clone());
536        assert_eq!(attachment.id, "test_id");
537        assert_eq!(attachment.url, url);
538    }
539
540    #[test]
541    fn test_attachment_serialization() {
542        let url = Url::parse("https://example.com/image.png").expect("Failed to parse URL");
543        let attachment = Attachment::new("test_id", url);
544        let json = serde_json::to_string(&attachment).expect("Failed to serialize attachment");
545        assert!(json.contains("test_id"));
546        assert!(json.contains("https://example.com/image.png"));
547    }
548
549    #[test]
550    fn test_attachment_deserialization() {
551        let json = r#"{"id":"test_id","url":"https://example.com/image.png"}"#;
552        let attachment: Attachment =
553            serde_json::from_str(json).expect("Failed to deserialize attachment");
554        assert_eq!(attachment.id, "test_id");
555        assert_eq!(attachment.url.as_str(), "https://example.com/image.png");
556    }
557
558    #[test]
559    fn test_schedule_every_display() {
560        assert_eq!(ScheduleEvery::Year.to_string(), "year");
561        assert_eq!(ScheduleEvery::Month.to_string(), "month");
562        assert_eq!(ScheduleEvery::TwoWeeks.to_string(), "twoWeeks");
563        assert_eq!(ScheduleEvery::Week.to_string(), "week");
564        assert_eq!(ScheduleEvery::Day.to_string(), "day");
565        assert_eq!(ScheduleEvery::Hour.to_string(), "hour");
566        assert_eq!(ScheduleEvery::Minute.to_string(), "minute");
567        assert_eq!(ScheduleEvery::Second.to_string(), "second");
568    }
569
570    #[test]
571    fn test_schedule_every_serialization() {
572        let json = serde_json::to_string(&ScheduleEvery::Day).expect("Failed to serialize Day");
573        assert_eq!(json, "\"day\"");
574
575        let json =
576            serde_json::to_string(&ScheduleEvery::TwoWeeks).expect("Failed to serialize TwoWeeks");
577        assert_eq!(json, "\"twoWeeks\"");
578    }
579
580    #[test]
581    fn test_schedule_every_deserialization() {
582        let every: ScheduleEvery =
583            serde_json::from_str("\"year\"").expect("Failed to deserialize year");
584        assert!(matches!(every, ScheduleEvery::Year));
585
586        let every: ScheduleEvery =
587            serde_json::from_str("\"month\"").expect("Failed to deserialize month");
588        assert!(matches!(every, ScheduleEvery::Month));
589
590        let every: ScheduleEvery =
591            serde_json::from_str("\"twoweeks\"").expect("Failed to deserialize twoweeks");
592        assert!(matches!(every, ScheduleEvery::TwoWeeks));
593
594        let every: ScheduleEvery =
595            serde_json::from_str("\"week\"").expect("Failed to deserialize week");
596        assert!(matches!(every, ScheduleEvery::Week));
597
598        let every: ScheduleEvery =
599            serde_json::from_str("\"day\"").expect("Failed to deserialize day");
600        assert!(matches!(every, ScheduleEvery::Day));
601
602        let every: ScheduleEvery =
603            serde_json::from_str("\"hour\"").expect("Failed to deserialize hour");
604        assert!(matches!(every, ScheduleEvery::Hour));
605
606        let every: ScheduleEvery =
607            serde_json::from_str("\"minute\"").expect("Failed to deserialize minute");
608        assert!(matches!(every, ScheduleEvery::Minute));
609
610        let every: ScheduleEvery =
611            serde_json::from_str("\"second\"").expect("Failed to deserialize second");
612        assert!(matches!(every, ScheduleEvery::Second));
613    }
614
615    #[test]
616    fn test_schedule_every_deserialization_invalid() {
617        let result: Result<ScheduleEvery, _> = serde_json::from_str("\"invalid\"");
618        assert!(result.is_err());
619    }
620
621    #[test]
622    fn test_schedule_interval_default() {
623        let interval = ScheduleInterval::default();
624        assert!(interval.year.is_none());
625        assert!(interval.month.is_none());
626        assert!(interval.day.is_none());
627        assert!(interval.weekday.is_none());
628        assert!(interval.hour.is_none());
629        assert!(interval.minute.is_none());
630        assert!(interval.second.is_none());
631    }
632
633    #[test]
634    fn test_schedule_interval_serialization() {
635        let interval = ScheduleInterval {
636            year: Some(24),
637            month: Some(12),
638            day: Some(25),
639            weekday: Some(1),
640            hour: Some(10),
641            minute: Some(30),
642            second: Some(0),
643        };
644        let json = serde_json::to_string(&interval).expect("Failed to serialize interval");
645        assert!(json.contains("\"year\":24"));
646        assert!(json.contains("\"month\":12"));
647        assert!(json.contains("\"day\":25"));
648    }
649
650    #[test]
651    fn test_notification_data_default() {
652        let data = NotificationData::default();
653        assert!(data.id != 0); // Should be a random ID
654        assert!(data.channel_id.is_none());
655        assert!(data.title.is_none());
656        assert!(data.body.is_none());
657        assert!(data.schedule.is_none());
658        assert!(!data.group_summary);
659        assert!(!data.ongoing);
660        assert!(!data.auto_cancel);
661        assert!(!data.silent);
662        assert!(data.inbox_lines.is_empty());
663        assert!(data.attachments.is_empty());
664        assert!(data.extra.is_empty());
665    }
666
667    #[test]
668    fn test_notification_data_serialization() {
669        let data = NotificationData {
670            id: 123,
671            title: Some("Test Title".to_string()),
672            body: Some("Test Body".to_string()),
673            ongoing: true,
674            ..Default::default()
675        };
676
677        let json = serde_json::to_string(&data).expect("Failed to serialize notification data");
678        assert!(json.contains("\"id\":123"));
679        assert!(json.contains("\"title\":\"Test Title\""));
680        assert!(json.contains("\"body\":\"Test Body\""));
681        assert!(json.contains("\"ongoing\":true"));
682    }
683
684    #[test]
685    fn test_pending_notification_getters() {
686        let json = r#"{
687            "id": 456,
688            "title": "Pending Title",
689            "body": "Pending Body",
690            "schedule": {"every": {"interval": "day", "count": 1}}
691        }"#;
692        let pending: PendingNotification =
693            serde_json::from_str(json).expect("Failed to deserialize pending notification");
694
695        assert_eq!(pending.id(), 456);
696        assert_eq!(pending.title(), Some("Pending Title"));
697        assert_eq!(pending.body(), Some("Pending Body"));
698        assert!(matches!(pending.schedule(), Schedule::Every { .. }));
699    }
700
701    #[test]
702    fn test_active_notification_getters() {
703        let json = r#"{
704            "id": 789,
705            "title": "Active Title",
706            "body": "Active Body",
707            "group": "test_group",
708            "groupSummary": true
709        }"#;
710        let active: ActiveNotification =
711            serde_json::from_str(json).expect("Failed to deserialize active notification");
712
713        assert_eq!(active.id(), 789);
714        assert_eq!(active.title(), Some("Active Title"));
715        assert_eq!(active.body(), Some("Active Body"));
716        assert_eq!(active.group(), Some("test_group"));
717        assert!(active.group_summary());
718        assert!(active.data().is_empty());
719        assert!(active.extra().is_empty());
720        assert!(active.attachments().is_empty());
721        assert!(active.action_type_id().is_none());
722        assert!(active.schedule().is_none());
723        assert!(active.sound().is_none());
724    }
725
726    #[cfg(target_os = "android")]
727    #[test]
728    fn test_importance_default() {
729        let importance = Importance::default();
730        assert!(matches!(importance, Importance::Default));
731    }
732
733    #[cfg(target_os = "android")]
734    #[test]
735    fn test_importance_serialization() {
736        assert_eq!(
737            serde_json::to_string(&Importance::None).expect("Failed to serialize Importance::None"),
738            "0"
739        );
740        assert_eq!(
741            serde_json::to_string(&Importance::Min).expect("Failed to serialize Importance::Min"),
742            "1"
743        );
744        assert_eq!(
745            serde_json::to_string(&Importance::Low).expect("Failed to serialize Importance::Low"),
746            "2"
747        );
748        assert_eq!(
749            serde_json::to_string(&Importance::Default)
750                .expect("Failed to serialize Importance::Default"),
751            "3"
752        );
753        assert_eq!(
754            serde_json::to_string(&Importance::High).expect("Failed to serialize Importance::High"),
755            "4"
756        );
757    }
758
759    #[cfg(target_os = "android")]
760    #[test]
761    fn test_visibility_serialization() {
762        assert_eq!(
763            serde_json::to_string(&Visibility::Secret)
764                .expect("Failed to serialize Visibility::Secret"),
765            "-1"
766        );
767        assert_eq!(
768            serde_json::to_string(&Visibility::Private)
769                .expect("Failed to serialize Visibility::Private"),
770            "0"
771        );
772        assert_eq!(
773            serde_json::to_string(&Visibility::Public)
774                .expect("Failed to serialize Visibility::Public"),
775            "1"
776        );
777    }
778
779    #[cfg(target_os = "android")]
780    #[test]
781    fn test_channel_builder() {
782        let channel = Channel::builder("test_id", "Test Channel")
783            .description("Test Description")
784            .sound("test_sound")
785            .lights(true)
786            .light_color("#FF0000")
787            .vibration(true)
788            .importance(Importance::High)
789            .visibility(Visibility::Public)
790            .build();
791
792        assert_eq!(channel.id(), "test_id");
793        assert_eq!(channel.name(), "Test Channel");
794        assert_eq!(channel.description(), Some("Test Description"));
795        assert_eq!(channel.sound(), Some("test_sound"));
796        assert!(channel.lights());
797        assert_eq!(channel.light_color(), Some("#FF0000"));
798        assert!(channel.vibration());
799        assert!(matches!(channel.importance(), Importance::High));
800        assert_eq!(channel.visibility(), Some(Visibility::Public));
801    }
802
803    #[cfg(target_os = "android")]
804    #[test]
805    fn test_channel_builder_minimal() {
806        let channel = Channel::builder("minimal_id", "Minimal Channel").build();
807
808        assert_eq!(channel.id(), "minimal_id");
809        assert_eq!(channel.name(), "Minimal Channel");
810        assert_eq!(channel.description(), None);
811        assert_eq!(channel.sound(), None);
812        assert!(!channel.lights());
813        assert_eq!(channel.light_color(), None);
814        assert!(!channel.vibration());
815        assert!(matches!(channel.importance(), Importance::Default));
816        assert_eq!(channel.visibility(), None);
817    }
818
819    #[test]
820    fn test_schedule_at_serialization() {
821        use time::OffsetDateTime;
822
823        let date = OffsetDateTime::now_utc();
824        let schedule = Schedule::At {
825            date,
826            repeating: true,
827            allow_while_idle: false,
828        };
829
830        let json = serde_json::to_string(&schedule).expect("Failed to serialize Schedule::At");
831        assert!(json.contains("\"at\""));
832        assert!(json.contains("\"date\""));
833        assert!(json.contains("\"repeating\":true"));
834        assert!(json.contains("\"allowWhileIdle\":false"));
835    }
836
837    #[test]
838    fn test_schedule_interval_variant() {
839        let schedule = Schedule::Interval {
840            interval: ScheduleInterval {
841                hour: Some(10),
842                minute: Some(30),
843                ..Default::default()
844            },
845            allow_while_idle: true,
846        };
847
848        let json =
849            serde_json::to_string(&schedule).expect("Failed to serialize Schedule::Interval");
850        assert!(json.contains("\"interval\""));
851        assert!(json.contains("\"hour\":10"));
852        assert!(json.contains("\"minute\":30"));
853        assert!(json.contains("\"allowWhileIdle\":true"));
854    }
855
856    #[test]
857    fn test_schedule_every_variant() {
858        let schedule = Schedule::Every {
859            interval: ScheduleEvery::Day,
860            count: 5,
861            allow_while_idle: false,
862        };
863
864        let json = serde_json::to_string(&schedule).expect("Failed to serialize Schedule::Every");
865        assert!(json.contains("\"every\""));
866        assert!(json.contains("\"interval\":\"day\""));
867        assert!(json.contains("\"count\":5"));
868    }
869}