1#[cfg(target_os = "macos")]
2use block::ConcreteBlock;
3use fission_core::{
4 CancelNotificationRequest, NotificationError, NotificationPermission,
5 NotificationPermissionRequest, NotificationReceipt, NotificationRequest, NotificationSchedule,
6 NotificationSettings, PushPlatform, PushRegistration, PushRegistrationRequest,
7 SetBadgeCountRequest, CANCEL_ALL_NOTIFICATIONS, CANCEL_NOTIFICATION, GET_NOTIFICATION_SETTINGS,
8 REGISTER_PUSH_NOTIFICATIONS, REQUEST_NOTIFICATION_PERMISSION, SCHEDULE_NOTIFICATION,
9 SET_BADGE_COUNT, SHOW_NOTIFICATION, UNREGISTER_PUSH_NOTIFICATIONS,
10};
11use fission_shell::async_host::AsyncRegistry;
12#[cfg(target_os = "ios")]
13use objc::{class, msg_send, sel, sel_impl};
14#[cfg(target_os = "macos")]
15use objc::{class, msg_send, sel, sel_impl};
16#[cfg(target_os = "macos")]
17use std::ffi::CStr;
18#[cfg(any(target_os = "ios", target_os = "macos"))]
19use std::os::raw::c_void;
20#[cfg(not(target_os = "ios"))]
21use std::process::Command;
22use std::sync::Arc;
23#[cfg(target_os = "macos")]
24use std::sync::{Condvar, Mutex};
25
26#[cfg(target_os = "ios")]
27#[link(name = "UIKit", kind = "framework")]
28extern "C" {}
29
30#[cfg(target_os = "macos")]
31#[link(name = "AppKit", kind = "framework")]
32extern "C" {}
33
34#[cfg(target_os = "macos")]
35#[link(name = "Foundation", kind = "framework")]
36extern "C" {}
37
38#[cfg(target_os = "macos")]
39#[link(name = "UserNotifications", kind = "framework")]
40extern "C" {}
41
42pub trait NotificationHost: Send + Sync + 'static {
44 fn request_permission(
49 &self,
50 request: NotificationPermissionRequest,
51 ) -> Result<NotificationSettings, NotificationError>;
52
53 fn settings(&self) -> Result<NotificationSettings, NotificationError>;
58
59 fn show(&self, request: NotificationRequest) -> Result<NotificationReceipt, NotificationError>;
65
66 fn schedule(
72 &self,
73 request: NotificationRequest,
74 ) -> Result<NotificationReceipt, NotificationError>;
75
76 fn cancel(&self, request: CancelNotificationRequest) -> Result<(), NotificationError>;
81
82 fn cancel_all(&self) -> Result<(), NotificationError>;
84
85 fn set_badge_count(&self, request: SetBadgeCountRequest) -> Result<(), NotificationError>;
90
91 fn register_push(
96 &self,
97 request: PushRegistrationRequest,
98 ) -> Result<PushRegistration, NotificationError>;
99
100 fn unregister_push(&self) -> Result<(), NotificationError>;
102}
103
104#[derive(Debug, Default)]
106pub struct UnsupportedNotificationHost;
107
108impl NotificationHost for UnsupportedNotificationHost {
109 fn request_permission(
110 &self,
111 _request: NotificationPermissionRequest,
112 ) -> Result<NotificationSettings, NotificationError> {
113 Ok(NotificationSettings {
114 permission: NotificationPermission::Unsupported,
115 ..Default::default()
116 })
117 }
118
119 fn settings(&self) -> Result<NotificationSettings, NotificationError> {
120 Ok(NotificationSettings {
121 permission: NotificationPermission::Unsupported,
122 ..Default::default()
123 })
124 }
125
126 fn show(
127 &self,
128 _request: NotificationRequest,
129 ) -> Result<NotificationReceipt, NotificationError> {
130 Err(NotificationError::unsupported("show"))
131 }
132
133 fn schedule(
134 &self,
135 _request: NotificationRequest,
136 ) -> Result<NotificationReceipt, NotificationError> {
137 Err(NotificationError::unsupported("schedule"))
138 }
139
140 fn cancel(&self, _request: CancelNotificationRequest) -> Result<(), NotificationError> {
141 Err(NotificationError::unsupported("cancel"))
142 }
143
144 fn cancel_all(&self) -> Result<(), NotificationError> {
145 Err(NotificationError::unsupported("cancel_all"))
146 }
147
148 fn set_badge_count(&self, _request: SetBadgeCountRequest) -> Result<(), NotificationError> {
149 Err(NotificationError::unsupported("set_badge_count"))
150 }
151
152 fn register_push(
153 &self,
154 _request: PushRegistrationRequest,
155 ) -> Result<PushRegistration, NotificationError> {
156 Err(NotificationError::unsupported("register_push"))
157 }
158
159 fn unregister_push(&self) -> Result<(), NotificationError> {
160 Err(NotificationError::unsupported("unregister_push"))
161 }
162}
163
164#[derive(Debug, Default)]
166pub struct MemoryNotificationHost;
167
168impl NotificationHost for MemoryNotificationHost {
169 fn request_permission(
170 &self,
171 request: NotificationPermissionRequest,
172 ) -> Result<NotificationSettings, NotificationError> {
173 Ok(NotificationSettings {
174 permission: NotificationPermission::Granted,
175 alerts: request.alerts,
176 badge: request.badge,
177 sound: request.sound,
178 scheduling: true,
179 push: false,
180 })
181 }
182
183 fn settings(&self) -> Result<NotificationSettings, NotificationError> {
184 Ok(NotificationSettings {
185 permission: NotificationPermission::Granted,
186 alerts: true,
187 badge: true,
188 sound: true,
189 scheduling: true,
190 push: false,
191 })
192 }
193
194 fn show(&self, request: NotificationRequest) -> Result<NotificationReceipt, NotificationError> {
195 Ok(NotificationReceipt {
196 id: request.id,
197 scheduled: false,
198 delivered: true,
199 })
200 }
201
202 fn schedule(
203 &self,
204 request: NotificationRequest,
205 ) -> Result<NotificationReceipt, NotificationError> {
206 Ok(NotificationReceipt {
207 id: request.id,
208 scheduled: !matches!(request.schedule, NotificationSchedule::Immediate),
209 delivered: matches!(request.schedule, NotificationSchedule::Immediate),
210 })
211 }
212
213 fn cancel(&self, _request: CancelNotificationRequest) -> Result<(), NotificationError> {
214 Ok(())
215 }
216
217 fn cancel_all(&self) -> Result<(), NotificationError> {
218 Ok(())
219 }
220
221 fn set_badge_count(&self, _request: SetBadgeCountRequest) -> Result<(), NotificationError> {
222 Ok(())
223 }
224
225 fn register_push(
226 &self,
227 _request: PushRegistrationRequest,
228 ) -> Result<PushRegistration, NotificationError> {
229 Ok(PushRegistration {
230 platform: PushPlatform::Other("memory".into()),
231 token: "memory-push-token".into(),
232 endpoint: None,
233 p256dh_key: None,
234 auth_secret: None,
235 })
236 }
237
238 fn unregister_push(&self) -> Result<(), NotificationError> {
239 Ok(())
240 }
241}
242
243#[derive(Debug, Default)]
244pub struct NativeNotificationHost;
245
246pub(crate) fn native_notification_host() -> impl NotificationHost {
247 NativeNotificationHost
248}
249
250impl NativeNotificationHost {
251 #[cfg(any(test, not(target_os = "macos")))]
252 fn supported() -> bool {
253 cfg!(target_os = "ios")
254 || cfg!(target_os = "macos")
255 || (cfg!(target_os = "linux") && command_exists("notify-send"))
256 }
257
258 #[cfg(any(test, not(target_os = "macos")))]
259 fn native_settings() -> NotificationSettings {
260 if Self::supported() {
261 NotificationSettings {
262 permission: NotificationPermission::Granted,
263 alerts: true,
264 badge: cfg!(any(target_os = "ios", target_os = "macos")),
265 sound: true,
266 scheduling: cfg!(any(target_os = "ios", target_os = "macos"))
267 || (cfg!(target_os = "linux") && command_exists("notify-send")),
268 push: false,
269 }
270 } else {
271 NotificationSettings {
272 permission: NotificationPermission::Unsupported,
273 ..Default::default()
274 }
275 }
276 }
277
278 fn show_now(&self, request: &NotificationRequest) -> Result<(), NotificationError> {
279 #[cfg(target_os = "ios")]
280 {
281 ios_register_local_notifications();
282 ios_show_local_notification(request, None);
283 return Ok(());
284 }
285
286 #[cfg(not(target_os = "ios"))]
287 {
288 if cfg!(target_os = "macos") {
289 #[cfg(target_os = "macos")]
290 {
291 macos_deliver_notification(request, None)?;
292 return Ok(());
293 }
294 }
295
296 if cfg!(target_os = "linux") {
297 if !command_exists("notify-send") {
298 return Err(NotificationError::unsupported("show"));
299 }
300 Command::new("notify-send")
301 .arg(&request.title)
302 .arg(&request.body)
303 .spawn()
304 .map_err(notification_command_error)?
305 .wait()
306 .map_err(notification_command_error)?;
307 return Ok(());
308 }
309
310 if cfg!(target_os = "windows") {
311 return Err(NotificationError::unsupported("show_windows_toast"));
312 }
313
314 Err(NotificationError::unsupported("show"))
315 }
316 }
317}
318
319impl NotificationHost for NativeNotificationHost {
320 fn request_permission(
321 &self,
322 _request: NotificationPermissionRequest,
323 ) -> Result<NotificationSettings, NotificationError> {
324 #[cfg(target_os = "macos")]
325 {
326 macos_request_notification_permission()
327 }
328 #[cfg(not(target_os = "macos"))]
329 {
330 #[cfg(target_os = "ios")]
331 ios_register_local_notifications();
332 Ok(Self::native_settings())
333 }
334 }
335
336 fn settings(&self) -> Result<NotificationSettings, NotificationError> {
337 #[cfg(target_os = "macos")]
338 {
339 macos_notification_settings()
340 }
341 #[cfg(not(target_os = "macos"))]
342 {
343 Ok(Self::native_settings())
344 }
345 }
346
347 fn show(&self, request: NotificationRequest) -> Result<NotificationReceipt, NotificationError> {
348 match request.schedule {
349 NotificationSchedule::Immediate => {
350 self.show_now(&request)?;
351 Ok(NotificationReceipt {
352 id: request.id,
353 scheduled: false,
354 delivered: true,
355 })
356 }
357 _ => Err(NotificationError::unsupported("schedule")),
358 }
359 }
360
361 fn schedule(
362 &self,
363 request: NotificationRequest,
364 ) -> Result<NotificationReceipt, NotificationError> {
365 match request.schedule {
366 NotificationSchedule::Immediate => self.show(request),
367 #[cfg(target_os = "ios")]
368 NotificationSchedule::AfterMillis(ms) => {
369 ios_register_local_notifications();
370 ios_show_local_notification(&request, Some(ms as f64 / 1000.0));
371 Ok(NotificationReceipt {
372 id: request.id,
373 scheduled: true,
374 delivered: false,
375 })
376 }
377 #[cfg(target_os = "ios")]
378 NotificationSchedule::AtUnixMillis(ms) => {
379 let now_ms = std::time::SystemTime::now()
380 .duration_since(std::time::UNIX_EPOCH)
381 .map(|duration| duration.as_millis() as u64)
382 .unwrap_or(ms);
383 ios_register_local_notifications();
384 ios_show_local_notification(
385 &request,
386 Some(ms.saturating_sub(now_ms) as f64 / 1000.0),
387 );
388 Ok(NotificationReceipt {
389 id: request.id,
390 scheduled: true,
391 delivered: false,
392 })
393 }
394 #[cfg(not(target_os = "ios"))]
395 NotificationSchedule::AfterMillis(ms) => {
396 if cfg!(target_os = "macos") {
397 #[cfg(target_os = "macos")]
398 {
399 macos_deliver_notification(&request, Some(ms as f64 / 1000.0))?;
400 return Ok(NotificationReceipt {
401 id: request.id,
402 scheduled: true,
403 delivered: false,
404 });
405 }
406 }
407 if !(cfg!(target_os = "linux") && command_exists("notify-send")) {
408 return Err(NotificationError::unsupported("schedule"));
409 }
410 let id = request.id.clone();
411 let request = request.clone();
412 std::thread::spawn(move || {
413 std::thread::sleep(std::time::Duration::from_millis(ms));
414 let host = NativeNotificationHost;
415 let _ = host.show_now(&request);
416 });
417 Ok(NotificationReceipt {
418 id,
419 scheduled: true,
420 delivered: false,
421 })
422 }
423 #[cfg(not(target_os = "ios"))]
424 NotificationSchedule::AtUnixMillis(ms) => {
425 let now_ms = std::time::SystemTime::now()
426 .duration_since(std::time::UNIX_EPOCH)
427 .map(|duration| duration.as_millis() as u64)
428 .unwrap_or(ms);
429 if cfg!(target_os = "macos") {
430 #[cfg(target_os = "macos")]
431 {
432 macos_deliver_notification(
433 &request,
434 Some(ms.saturating_sub(now_ms) as f64 / 1000.0),
435 )?;
436 return Ok(NotificationReceipt {
437 id: request.id,
438 scheduled: true,
439 delivered: false,
440 });
441 }
442 }
443 if !(cfg!(target_os = "linux") && command_exists("notify-send")) {
444 return Err(NotificationError::unsupported("schedule"));
445 }
446 let id = request.id.clone();
447 let request = request.clone();
448 std::thread::spawn(move || {
449 std::thread::sleep(std::time::Duration::from_millis(ms.saturating_sub(now_ms)));
450 let host = NativeNotificationHost;
451 let _ = host.show_now(&request);
452 });
453 Ok(NotificationReceipt {
454 id,
455 scheduled: true,
456 delivered: false,
457 })
458 }
459 }
460 }
461
462 fn cancel(&self, request: CancelNotificationRequest) -> Result<(), NotificationError> {
463 #[cfg(target_os = "macos")]
464 {
465 macos_cancel_notification(&request.id.0);
466 Ok(())
467 }
468 #[cfg(not(target_os = "macos"))]
469 {
470 let _ = request;
471 Err(NotificationError::unsupported("cancel"))
472 }
473 }
474
475 fn cancel_all(&self) -> Result<(), NotificationError> {
476 #[cfg(target_os = "macos")]
477 {
478 macos_cancel_all_notifications();
479 Ok(())
480 }
481 #[cfg(not(target_os = "macos"))]
482 {
483 Err(NotificationError::unsupported("cancel_all"))
484 }
485 }
486
487 fn set_badge_count(&self, request: SetBadgeCountRequest) -> Result<(), NotificationError> {
488 #[cfg(target_os = "ios")]
489 {
490 ios_set_badge_count(request.count);
491 return Ok(());
492 }
493 #[cfg(target_os = "macos")]
494 {
495 macos_set_badge_count(request.count);
496 return Ok(());
497 }
498 #[cfg(not(any(target_os = "ios", target_os = "macos")))]
499 {
500 let _ = request;
501 Err(NotificationError::unsupported("set_badge_count"))
502 }
503 }
504
505 fn register_push(
506 &self,
507 _request: PushRegistrationRequest,
508 ) -> Result<PushRegistration, NotificationError> {
509 Err(NotificationError::unsupported("register_push"))
510 }
511
512 fn unregister_push(&self) -> Result<(), NotificationError> {
513 Err(NotificationError::unsupported("unregister_push"))
514 }
515}
516
517#[cfg(target_os = "ios")]
518fn ios_register_local_notifications() {
519 unsafe {
520 let app: *mut objc::runtime::Object = msg_send![class!(UIApplication), sharedApplication];
521 if app.is_null() {
522 return;
523 }
524 let settings: *mut objc::runtime::Object = msg_send![
525 class!(UIUserNotificationSettings),
526 settingsForTypes: 7usize
527 categories: std::ptr::null_mut::<objc::runtime::Object>()
528 ];
529 if !settings.is_null() {
530 let _: () = msg_send![app, registerUserNotificationSettings: settings];
531 }
532 }
533}
534
535#[cfg(target_os = "ios")]
536fn ios_show_local_notification(request: &NotificationRequest, delay_seconds: Option<f64>) {
537 unsafe {
538 let notification: *mut objc::runtime::Object = msg_send![class!(UILocalNotification), new];
539 if notification.is_null() {
540 return;
541 }
542 let title = ns_string(&request.title);
543 let body = ns_string(&request.body);
544 let _: () = msg_send![notification, setAlertTitle: title];
545 let _: () = msg_send![notification, setAlertBody: body];
546 if !matches!(request.sound, fission_core::NotificationSound::Silent) {
547 let default_sound: *mut objc::runtime::Object =
548 msg_send![class!(UILocalNotification), defaultSoundName];
549 let _: () = msg_send![notification, setSoundName: default_sound];
550 }
551 if let Some(badge) = request.badge {
552 let _: () = msg_send![notification, setApplicationIconBadgeNumber: badge as isize];
553 }
554 let app: *mut objc::runtime::Object = msg_send![class!(UIApplication), sharedApplication];
555 if app.is_null() {
556 return;
557 }
558 if let Some(delay) = delay_seconds {
559 let date: *mut objc::runtime::Object =
560 msg_send![class!(NSDate), dateWithTimeIntervalSinceNow: delay.max(0.0)];
561 let _: () = msg_send![notification, setFireDate: date];
562 let _: () = msg_send![app, scheduleLocalNotification: notification];
563 } else {
564 let _: () = msg_send![app, presentLocalNotificationNow: notification];
565 }
566 }
567}
568
569#[cfg(target_os = "ios")]
570fn ios_set_badge_count(count: Option<u32>) {
571 unsafe {
572 let app: *mut objc::runtime::Object = msg_send![class!(UIApplication), sharedApplication];
573 if !app.is_null() {
574 let _: () = msg_send![app, setApplicationIconBadgeNumber: count.unwrap_or(0) as isize];
575 }
576 }
577}
578
579#[cfg(target_os = "macos")]
580fn macos_request_notification_permission() -> Result<NotificationSettings, NotificationError> {
581 let pair = Arc::new((Mutex::new(None), Condvar::new()));
582 let pair_for_block = pair.clone();
583 let block = ConcreteBlock::new(move |granted: bool, _error: *mut objc::runtime::Object| {
584 let (lock, cvar) = &*pair_for_block;
585 if let Ok(mut result) = lock.lock() {
586 *result = Some(granted);
587 cvar.notify_all();
588 }
589 })
590 .copy();
591 unsafe {
592 let center: *mut objc::runtime::Object =
593 msg_send![class!(UNUserNotificationCenter), currentNotificationCenter];
594 if center.is_null() {
595 return Err(NotificationError::unsupported("notifications"));
596 }
597 let options = 1usize | 2usize | 4usize;
598 let _: () = msg_send![
599 center,
600 requestAuthorizationWithOptions: options
601 completionHandler: &*block
602 ];
603 }
604 let (lock, cvar) = &*pair;
605 let guard = lock.lock().unwrap();
606 let (guard, _) = cvar
607 .wait_timeout_while(guard, std::time::Duration::from_secs(30), |value| {
608 value.is_none()
609 })
610 .unwrap();
611 let granted = (*guard).unwrap_or(false);
612 Ok(NotificationSettings {
613 permission: if granted {
614 NotificationPermission::Granted
615 } else {
616 NotificationPermission::Denied
617 },
618 alerts: granted,
619 badge: granted,
620 sound: granted,
621 scheduling: granted,
622 push: false,
623 })
624}
625
626#[cfg(target_os = "macos")]
627fn macos_notification_settings() -> Result<NotificationSettings, NotificationError> {
628 let pair = Arc::new((Mutex::new(None), Condvar::new()));
629 let pair_for_block = pair.clone();
630 let block = ConcreteBlock::new(move |settings: *mut objc::runtime::Object| {
631 let status: i64 = if settings.is_null() {
632 0
633 } else {
634 unsafe { msg_send![settings, authorizationStatus] }
635 };
636 let permission = match status {
637 2 => NotificationPermission::Granted,
638 3 | 4 => NotificationPermission::Provisional,
639 1 => NotificationPermission::Denied,
640 _ => NotificationPermission::NotDetermined,
641 };
642 let enabled = matches!(
643 permission,
644 NotificationPermission::Granted | NotificationPermission::Provisional
645 );
646 let (lock, cvar) = &*pair_for_block;
647 if let Ok(mut result) = lock.lock() {
648 *result = Some(NotificationSettings {
649 permission,
650 alerts: enabled,
651 badge: enabled,
652 sound: enabled,
653 scheduling: enabled,
654 push: false,
655 });
656 cvar.notify_all();
657 }
658 })
659 .copy();
660 unsafe {
661 let center: *mut objc::runtime::Object =
662 msg_send![class!(UNUserNotificationCenter), currentNotificationCenter];
663 if center.is_null() {
664 return Err(NotificationError::unsupported("notifications"));
665 }
666 let _: () = msg_send![center, getNotificationSettingsWithCompletionHandler: &*block];
667 }
668 let (lock, cvar) = &*pair;
669 let guard = lock.lock().unwrap();
670 let (guard, _) = cvar
671 .wait_timeout_while(guard, std::time::Duration::from_secs(30), |value| {
672 value.is_none()
673 })
674 .unwrap();
675 Ok(guard.clone().unwrap_or(NotificationSettings {
676 permission: NotificationPermission::NotDetermined,
677 ..Default::default()
678 }))
679}
680
681#[cfg(target_os = "macos")]
682fn macos_deliver_notification(
683 request: &NotificationRequest,
684 delay_seconds: Option<f64>,
685) -> Result<(), NotificationError> {
686 let settings = macos_request_notification_permission()?;
687 if !matches!(
688 settings.permission,
689 NotificationPermission::Granted | NotificationPermission::Provisional
690 ) {
691 return Err(NotificationError::new(
692 "permission_denied",
693 "macOS notification permission is not granted",
694 ));
695 }
696
697 let pair = Arc::new((Mutex::new(None), Condvar::new()));
698 let pair_for_block = pair.clone();
699 let block = ConcreteBlock::new(move |error: *mut objc::runtime::Object| {
700 let message = if error.is_null() {
701 None
702 } else {
703 Some(macos_error_description(error))
704 };
705 let (lock, cvar) = &*pair_for_block;
706 if let Ok(mut result) = lock.lock() {
707 *result = Some(message);
708 cvar.notify_all();
709 }
710 })
711 .copy();
712
713 unsafe {
714 let content: *mut objc::runtime::Object =
715 msg_send![class!(UNMutableNotificationContent), new];
716 if content.is_null() {
717 return Err(NotificationError::unsupported("notification_content"));
718 }
719 let title = ns_string(&request.title);
720 let body = ns_string(&request.body);
721 let _: () = msg_send![content, setTitle: title];
722 let _: () = msg_send![content, setBody: body];
723 if let Some(subtitle) = request.subtitle.as_deref() {
724 let subtitle = ns_string(subtitle);
725 let _: () = msg_send![content, setSubtitle: subtitle];
726 }
727 if !matches!(request.sound, fission_core::NotificationSound::Silent) {
728 let sound: *mut objc::runtime::Object =
729 msg_send![class!(UNNotificationSound), defaultSound];
730 let _: () = msg_send![content, setSound: sound];
731 }
732 if let Some(badge) = request.badge {
733 let badge: *mut objc::runtime::Object =
734 msg_send![class!(NSNumber), numberWithUnsignedInteger: badge as usize];
735 let _: () = msg_send![content, setBadge: badge];
736 }
737
738 let trigger: *mut objc::runtime::Object = if let Some(delay) = delay_seconds {
739 msg_send![
740 class!(UNTimeIntervalNotificationTrigger),
741 triggerWithTimeInterval: delay.max(1.0)
742 repeats: false
743 ]
744 } else {
745 std::ptr::null_mut()
746 };
747 let identifier = ns_string(&request.id.0);
748 let notification_request: *mut objc::runtime::Object = msg_send![
749 class!(UNNotificationRequest),
750 requestWithIdentifier: identifier
751 content: content
752 trigger: trigger
753 ];
754 if notification_request.is_null() {
755 return Err(NotificationError::unsupported("notification_request"));
756 }
757 let center: *mut objc::runtime::Object =
758 msg_send![class!(UNUserNotificationCenter), currentNotificationCenter];
759 if center.is_null() {
760 return Err(NotificationError::unsupported("notifications"));
761 }
762 let _: () = msg_send![center, addNotificationRequest: notification_request withCompletionHandler: &*block];
763 }
764
765 let (lock, cvar) = &*pair;
766 let guard = lock.lock().unwrap();
767 let (guard, _) = cvar
768 .wait_timeout_while(guard, std::time::Duration::from_secs(30), |value| {
769 value.is_none()
770 })
771 .unwrap();
772 if let Some(Some(message)) = guard.clone() {
773 Err(NotificationError::new("host_error", message))
774 } else {
775 Ok(())
776 }
777}
778
779#[cfg(target_os = "macos")]
780fn macos_error_description(error: *mut objc::runtime::Object) -> String {
781 unsafe {
782 let description: *mut objc::runtime::Object = msg_send![error, localizedDescription];
783 ns_string_to_string(description).unwrap_or_else(|| "macOS notification error".into())
784 }
785}
786
787#[cfg(target_os = "macos")]
788fn macos_cancel_notification(id: &str) {
789 unsafe {
790 let center: *mut objc::runtime::Object =
791 msg_send![class!(UNUserNotificationCenter), currentNotificationCenter];
792 if center.is_null() {
793 return;
794 }
795 let identifier = ns_string(id);
796 let ids: *mut objc::runtime::Object =
797 msg_send![class!(NSArray), arrayWithObject: identifier];
798 let _: () = msg_send![center, removePendingNotificationRequestsWithIdentifiers: ids];
799 let _: () = msg_send![center, removeDeliveredNotificationsWithIdentifiers: ids];
800 }
801}
802
803#[cfg(target_os = "macos")]
804fn macos_cancel_all_notifications() {
805 unsafe {
806 let center: *mut objc::runtime::Object =
807 msg_send![class!(UNUserNotificationCenter), currentNotificationCenter];
808 if center.is_null() {
809 return;
810 }
811 let _: () = msg_send![center, removeAllPendingNotificationRequests];
812 let _: () = msg_send![center, removeAllDeliveredNotifications];
813 }
814}
815
816#[cfg(target_os = "macos")]
817fn macos_set_badge_count(count: Option<u32>) {
818 unsafe {
819 let app: *mut objc::runtime::Object = msg_send![class!(NSApplication), sharedApplication];
820 if app.is_null() {
821 return;
822 }
823 let dock_tile: *mut objc::runtime::Object = msg_send![app, dockTile];
824 if dock_tile.is_null() {
825 return;
826 }
827 let label = count
828 .filter(|count| *count > 0)
829 .map(|count| ns_string(&count.to_string()))
830 .unwrap_or(std::ptr::null_mut());
831 let _: () = msg_send![dock_tile, setBadgeLabel: label];
832 }
833}
834
835#[cfg(any(target_os = "ios", target_os = "macos"))]
836fn ns_string(value: &str) -> *mut objc::runtime::Object {
837 unsafe {
838 let string: *mut objc::runtime::Object = msg_send![class!(NSString), alloc];
839 msg_send![
840 string,
841 initWithBytes: value.as_ptr() as *const c_void
842 length: value.len()
843 encoding: 4usize
844 ]
845 }
846}
847
848#[cfg(target_os = "macos")]
849fn ns_string_to_string(value: *mut objc::runtime::Object) -> Option<String> {
850 if value.is_null() {
851 return None;
852 }
853 unsafe {
854 let ptr: *const std::os::raw::c_char = msg_send![value, UTF8String];
855 (!ptr.is_null()).then(|| CStr::from_ptr(ptr).to_string_lossy().into_owned())
856 }
857}
858
859fn command_exists(name: &str) -> bool {
860 std::env::var_os("PATH")
861 .and_then(|paths| {
862 std::env::split_paths(&paths)
863 .map(|path| path.join(name))
864 .find(|path| path.is_file())
865 })
866 .is_some()
867}
868
869#[cfg(not(target_os = "ios"))]
870fn notification_command_error(error: std::io::Error) -> NotificationError {
871 NotificationError::new("host_error", error.to_string())
872}
873
874pub(crate) fn register_notification_capabilities(
875 async_registry: &mut AsyncRegistry,
876 host: Arc<dyn NotificationHost>,
877) {
878 let request_host = host.clone();
879 async_registry.register_operation_capability(
880 REQUEST_NOTIFICATION_PERMISSION,
881 move |request, _| {
882 let host = request_host.clone();
883 async move { host.request_permission(request) }
884 },
885 );
886
887 let settings_host = host.clone();
888 async_registry.register_operation_capability(GET_NOTIFICATION_SETTINGS, move |(), _| {
889 let host = settings_host.clone();
890 async move { host.settings() }
891 });
892
893 let show_host = host.clone();
894 async_registry.register_operation_capability(SHOW_NOTIFICATION, move |request, _| {
895 let host = show_host.clone();
896 async move { host.show(request) }
897 });
898
899 let schedule_host = host.clone();
900 async_registry.register_operation_capability(SCHEDULE_NOTIFICATION, move |request, _| {
901 let host = schedule_host.clone();
902 async move { host.schedule(request) }
903 });
904
905 let cancel_host = host.clone();
906 async_registry.register_operation_capability(CANCEL_NOTIFICATION, move |request, _| {
907 let host = cancel_host.clone();
908 async move { host.cancel(request) }
909 });
910
911 let cancel_all_host = host.clone();
912 async_registry.register_operation_capability(CANCEL_ALL_NOTIFICATIONS, move |(), _| {
913 let host = cancel_all_host.clone();
914 async move { host.cancel_all() }
915 });
916
917 let badge_host = host.clone();
918 async_registry.register_operation_capability(SET_BADGE_COUNT, move |request, _| {
919 let host = badge_host.clone();
920 async move { host.set_badge_count(request) }
921 });
922
923 let push_host = host.clone();
924 async_registry.register_operation_capability(REGISTER_PUSH_NOTIFICATIONS, move |request, _| {
925 let host = push_host.clone();
926 async move { host.register_push(request) }
927 });
928
929 async_registry.register_operation_capability(UNREGISTER_PUSH_NOTIFICATIONS, move |(), _| {
930 let host = host.clone();
931 async move { host.unregister_push() }
932 });
933}
934
935#[cfg(test)]
936mod tests {
937 use super::*;
938 use fission_core::NotificationId;
939
940 #[test]
941 fn unsupported_host_reports_permission_without_panicking() {
942 let host = UnsupportedNotificationHost;
943 let settings = host
944 .request_permission(NotificationPermissionRequest::default())
945 .unwrap();
946 assert_eq!(settings.permission, NotificationPermission::Unsupported);
947 assert_eq!(
948 host.show(NotificationRequest::default()).unwrap_err().code,
949 "unsupported"
950 );
951 }
952
953 #[test]
954 fn memory_host_returns_receipts() {
955 let host = MemoryNotificationHost;
956 let receipt = host
957 .show(NotificationRequest {
958 id: NotificationId::new("n1"),
959 title: "Title".into(),
960 body: "Body".into(),
961 ..Default::default()
962 })
963 .unwrap();
964 assert_eq!(receipt.id, NotificationId::new("n1"));
965 assert!(receipt.delivered);
966 }
967
968 #[test]
969 fn native_host_settings_are_honest_about_support() {
970 let settings = NativeNotificationHost::native_settings();
971 if NativeNotificationHost::supported() {
972 assert_eq!(settings.permission, NotificationPermission::Granted);
973 assert!(settings.alerts);
974 assert!(!settings.push);
975 } else {
976 assert_eq!(settings.permission, NotificationPermission::Unsupported);
977 assert!(!settings.alerts);
978 }
979 }
980}