diff --git a/Cargo.toml b/Cargo.toml index 3436f9996..d7839400c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tao" -version = "0.30.7" +version = "0.30.8" description = "Cross-platform window manager library." authors = [ "Tauri Programme within The Commons Conservancy", @@ -32,6 +32,7 @@ serde = [ "dep:serde", "dpi/serde" ] rwh_04 = [ "dep:rwh_04" ] rwh_05 = [ "dep:rwh_05" ] rwh_06 = [ "dep:rwh_06" ] +push-notifications = [] [workspace] members = [ "tao-macros" ] @@ -61,11 +62,13 @@ parking_lot = "0.12" unicode-segmentation = "1.11" windows-version = "0.1" windows-core = "0.58" +windows-async = "0.1.1" [target."cfg(target_os = \"windows\")".dependencies.windows] version = "0.58" features = [ "implement", + "Networking_PushNotifications", "Win32_Devices_HumanInterfaceDevice", "Win32_Foundation", "Win32_Globalization", diff --git a/src/event.rs b/src/event.rs index d6efde565..30d9bd088 100644 --- a/src/event.rs +++ b/src/event.rs @@ -40,6 +40,7 @@ use std::path::PathBuf; use std::time::Instant; +use crate::push::PushToken; use crate::{ dpi::{PhysicalPosition, PhysicalSize}, keyboard::{self, ModifiersState}, @@ -143,6 +144,17 @@ pub enum Event<'a, T: 'static> { /// - **Other**: Unsupported. #[non_exhaustive] Reopen { has_visible_windows: bool }, + + /// ## Push Tokens + /// + /// Emitted when registration completes and an application push token is made available; on Apple + /// platforms, this is the APNS token. On Android, this is the FCM token. + PushRegistration(PushToken), + + /// ## Push Token Errors + /// + /// Emitted when push token registration fails. + PushRegistrationError(String), } impl Clone for Event<'static, T> { @@ -171,6 +183,8 @@ impl Clone for Event<'static, T> { } => Reopen { has_visible_windows: *has_visible_windows, }, + PushRegistration(token) => PushRegistration(token.clone()), + PushRegistrationError(error) => PushRegistrationError(error.clone()), } } } @@ -195,6 +209,8 @@ impl<'a, T> Event<'a, T> { } => Ok(Reopen { has_visible_windows, }), + PushRegistration(token) => Ok(PushRegistration(token)), + PushRegistrationError(error) => Err(PushRegistrationError(error)), } } @@ -221,6 +237,8 @@ impl<'a, T> Event<'a, T> { } => Some(Reopen { has_visible_windows, }), + PushRegistration(token) => Some(PushRegistration(token)), + PushRegistrationError(error) => Some(PushRegistrationError(error)), } } } diff --git a/src/event_loop.rs b/src/event_loop.rs index c27578d39..53996908c 100644 --- a/src/event_loop.rs +++ b/src/event_loop.rs @@ -13,9 +13,6 @@ //! [create_proxy]: crate::event_loop::EventLoop::create_proxy //! [event_loop_proxy]: crate::event_loop::EventLoopProxy //! [send_event]: crate::event_loop::EventLoopProxy::send_event -use std::time::Instant; -use std::{error, fmt, marker::PhantomData, ops::Deref}; - use crate::{ dpi::PhysicalPosition, error::ExternalError, @@ -24,6 +21,12 @@ use crate::{ platform_impl, window::{ProgressBarState, Theme}, }; +use std::time::Instant; +use std::{error, fmt, marker::PhantomData, ops::Deref}; + +#[cfg(feature = "push-notifications")] +#[cfg(any(windows))] +use windows::Networking::PushNotifications::PushNotificationChannel; /// Provides a way to retrieve events from the system and from the windows that were registered to /// the events loop. @@ -319,6 +322,18 @@ impl EventLoopWindowTarget { ))] self.p.set_theme(theme) } + + /// Sets the push channel for the application. + /// + /// ## Platform-specific + /// + /// - **Windows only.** Other platforms deliver the push channel or token via other means. + #[cfg(any(windows))] + #[cfg(feature = "push-notifications")] + #[inline] + pub fn set_push_channel(&self, channel: Option) { + self.p.set_push_channel(channel) + } } #[cfg(feature = "rwh_05")] diff --git a/src/lib.rs b/src/lib.rs index f5f2feda4..3e6451ddf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -180,6 +180,7 @@ mod icon; pub mod keyboard; pub mod monitor; mod platform_impl; +pub mod push; pub mod window; diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 59d0d7c3d..195213028 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -17,6 +17,14 @@ use crate::{ }; use windows::Win32::UI::Input::KeyboardAndMouse::*; +#[cfg(feature = "push-notifications")] +use windows::Networking::PushNotifications::{ + PushNotificationChannel, PushNotificationChannelManager, +}; + +#[cfg(feature = "push-notifications")] +use {windows::Foundation::AsyncStatus, windows::Foundation::IAsyncOperation}; + pub type HWND = isize; pub type HMENU = isize; @@ -440,3 +448,72 @@ impl IconExtWindows for Icon { Ok(Icon { inner: win_icon }) } } + +/// Additional methods on `PushNotifications` that are specific to Windows. +#[cfg(feature = "push-notifications")] +pub trait PushNotificationsExtWindows { + /// Setup Windows Notifications System, the Windows equivalent to APNS. + /// + /// This call yields an `IAsyncOperation` which must be awaited to obtain the underlying + /// `PushNotificationChannel`. + /// + /// Return error codes are documented below: + /// (None yet). + fn setup_wns() -> Result<(), u8> { + let mgr = PushNotificationChannelManager::GetDefault(); + let mgr = match mgr { + Ok(mgr) => mgr, + Err(_) => { + return Err(1); + } + }; + let register_op = match mgr.CreatePushNotificationChannelForApplicationAsync() { + Ok(channel) => channel, + Err(_) => { + return Err(2); + } + }; + // Attach callback + attach_callback(register_op, |result| match result { + Ok(value) => register_push_channel(value), + Err(e) => println!("Operation failed with error: {:?}", e), + }) + .expect("failed to attach callback for windows push notification token"); + + Ok(()) + } +} + +#[cfg(feature = "push-notifications")] +fn register_push_channel(_channel: PushNotificationChannel) { + eprintln!("would register channel") +} + +#[cfg(feature = "push-notifications")] +fn attach_callback(operation: IAsyncOperation, callback: F) -> windows::core::Result<()> +where + T: windows::core::RuntimeType + 'static, + F: FnOnce(windows::core::Result) + Send + Clone + Copy + 'static, +{ + unsafe { + operation.SetCompleted(&windows::Foundation::AsyncOperationCompletedHandler::new( + move |op, _| { + let result = match op.unwrap().Status()? { + AsyncStatus::Completed => Ok(op.unwrap().GetResults()), + AsyncStatus::Canceled => Err(windows::core::Error::new::( + windows::core::HRESULT(0x800704C7u32 as i32), // Operation canceled + "Operation was canceled".into(), + )), + AsyncStatus::Error => Err(windows::core::Error::new::( + op.unwrap().ErrorCode().unwrap(), // Operation failed + "Operation failed".into(), + )), + AsyncStatus::Started => unreachable!(), + _ => unreachable!(), + }; + callback(result.expect("empty waiter")); + Ok(()) + }, + )) + } +} diff --git a/src/platform_impl/macos/app_delegate.rs b/src/platform_impl/macos/app_delegate.rs index 2bcf7b3b3..f8826ec51 100644 --- a/src/platform_impl/macos/app_delegate.rs +++ b/src/platform_impl/macos/app_delegate.rs @@ -9,8 +9,11 @@ use cocoa::{ foundation::NSString, }; use objc::{ + class, declare::ClassDecl, + msg_send, runtime::{Class, Object, Sel, BOOL}, + sel, sel_impl, }; use std::{ cell::{RefCell, RefMut}, @@ -63,6 +66,14 @@ lazy_static! { sel!(applicationSupportsSecureRestorableState:), application_supports_secure_restorable_state as extern "C" fn(&Object, Sel, id) -> BOOL, ); + decl.add_method( + sel!(application:didRegisterForRemoteNotificationsWithDeviceToken:), + did_register_for_apns as extern "C" fn(&Object, Sel, id, id), + ); + decl.add_method( + sel!(application:didFailToRegisterForRemoteNotificationsWithError:), + did_fail_to_register_for_apns as extern "C" fn(&Object, _: Sel, id, id), + ); decl.add_ivar::<*mut c_void>(AUX_DELEGATE_STATE_NAME); AppDelegateClass(decl.register()) @@ -100,8 +111,26 @@ extern "C" fn dealloc(this: &Object, _: Sel) { } } +#[cfg(feature = "push-notifications")] +fn register_push_notifications() { + // register for push notifications. this call is inert on macOS unless the app is entitled to + // access an APS environment. see: + // https://developer.apple.com/documentation/usernotifications/registering-your-app-with-apns + // and: + // https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_aps-environment + let shared_app = get_shared_application(); + unsafe { + // registerForRemoteNotifications() + let _: () = msg_send![shared_app, registerForRemoteNotifications]; + }; +} + extern "C" fn did_finish_launching(this: &Object, _: Sel, _: id) { trace!("Triggered `applicationDidFinishLaunching`"); + + #[cfg(feature = "push-notifications")] + register_push_notifications(); + AppState::launched(this); trace!("Completed `applicationDidFinishLaunching`"); } @@ -147,3 +176,62 @@ extern "C" fn application_supports_secure_restorable_state(_: &Object, _: Sel, _ trace!("Completed `applicationSupportsSecureRestorableState`"); objc::runtime::YES } + +// application(_:didRegisterForRemoteNotificationsWithDeviceToken:) +extern "C" fn did_register_for_apns(_: &Object, _: Sel, _: id, token_data: id) { + trace!("Triggered `didRegisterForRemoteNotificationsWithDeviceToken`"); + let token_bytes = unsafe { + if token_data.is_null() { + trace!("Token data is null; ignoring"); + return; + } + let is_nsdata: bool = msg_send![token_data, isKindOfClass:class!(NSData)]; + if !is_nsdata { + trace!("Token data is not an NSData object"); + return; + } + let bytes: *const u8 = msg_send![token_data, bytes]; + let length: usize = msg_send![token_data, length]; + std::slice::from_raw_parts(bytes, length).to_vec() + }; + AppState::did_register_push_token(token_bytes); + trace!("Completed `didRegisterForRemoteNotificationsWithDeviceToken`"); +} + +// application(_:didFailToRegisterForRemoteNotificationsWithError:) +extern "C" fn did_fail_to_register_for_apns(_: &Object, _: Sel, _: id, err: *mut Object) { + trace!("Triggered `didFailToRegisterForRemoteNotificationsWithError`"); + + let error_string = unsafe { + if err.is_null() { + "Unknown error (null error object)".to_string() + } else { + // Verify it's an NSError + let is_error: bool = msg_send![err, isKindOfClass:class!(NSError)]; + if !is_error { + trace!("Invalid error object type for push registration failure"); + return; + } + + // Get the localizedDescription + let description: *mut Object = msg_send![err, localizedDescription]; + if description.is_null() { + trace!("Error had no description"); + return; + } + + // Convert NSString to str + let utf8: *const u8 = msg_send![description, UTF8String]; + let len: usize = msg_send![description, lengthOfBytesUsingEncoding:4]; + let bytes = std::slice::from_raw_parts(utf8, len); + String::from_utf8_lossy(bytes).to_string() + } + }; + + AppState::did_fail_to_register_push_token(error_string); + trace!("Completed `didFailToRegisterForRemoteNotificationsWithError`"); +} + +fn get_shared_application() -> *mut Object { + unsafe { msg_send![class!(NSApplication), sharedApplication] } +} diff --git a/src/platform_impl/macos/app_state.rs b/src/platform_impl/macos/app_state.rs index 65125cd4e..301449a3c 100644 --- a/src/platform_impl/macos/app_state.rs +++ b/src/platform_impl/macos/app_state.rs @@ -416,6 +416,16 @@ impl AppState { (_, ControlFlow::Poll) => HANDLER.waker().start(), } } + + pub fn did_register_push_token(token_data: Vec) { + HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::PushRegistration( + token_data, + ))); + } + + pub fn did_fail_to_register_push_token(err: String) { + HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::PushRegistrationError(err))); + } } /// A hack to make activation of multiple windows work when creating them before diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index caf53928c..6dc97b50f 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -6,8 +6,30 @@ mod runner; +use crate::{ + dpi::{PhysicalPosition, PhysicalSize, PixelUnit}, + error::ExternalError, + event::{DeviceEvent, Event, Force, RawKeyEvent, Touch, TouchPhase, WindowEvent}, + event_loop::{ControlFlow, DeviceEventFilter, EventLoopClosed, EventLoopWindowTarget as RootELW}, + keyboard::{KeyCode, ModifiersState}, + monitor::MonitorHandle as RootMonitorHandle, + platform_impl::platform::{ + dark_mode::try_window_theme, + dpi::{become_dpi_aware, dpi_to_scale_factor, enable_non_client_dpi_scaling}, + keyboard::is_msg_keyboard_related, + keyboard_layout::LAYOUT_CACHE, + minimal_ime::is_msg_ime_related, + monitor::{self, MonitorHandle}, + raw_input, util, + window::set_skip_taskbar, + window_state::{CursorFlags, WindowFlags, WindowState}, + wrap_device_id, WindowId, DEVICE_ID, + }, + window::{Fullscreen, Theme, WindowId as RootWindowId}, +}; use crossbeam_channel::{self as channel, Receiver, Sender}; use parking_lot::Mutex; +use runner::{EventLoopRunner, EventLoopRunnerShared}; use std::{ cell::Cell, collections::VecDeque, @@ -43,28 +65,8 @@ use windows::{ }, }; -use crate::{ - dpi::{PhysicalPosition, PhysicalSize, PixelUnit}, - error::ExternalError, - event::{DeviceEvent, Event, Force, RawKeyEvent, Touch, TouchPhase, WindowEvent}, - event_loop::{ControlFlow, DeviceEventFilter, EventLoopClosed, EventLoopWindowTarget as RootELW}, - keyboard::{KeyCode, ModifiersState}, - monitor::MonitorHandle as RootMonitorHandle, - platform_impl::platform::{ - dark_mode::try_window_theme, - dpi::{become_dpi_aware, dpi_to_scale_factor, enable_non_client_dpi_scaling}, - keyboard::is_msg_keyboard_related, - keyboard_layout::LAYOUT_CACHE, - minimal_ime::is_msg_ime_related, - monitor::{self, MonitorHandle}, - raw_input, util, - window::set_skip_taskbar, - window_state::{CursorFlags, WindowFlags, WindowState}, - wrap_device_id, WindowId, DEVICE_ID, - }, - window::{Fullscreen, Theme, WindowId as RootWindowId}, -}; -use runner::{EventLoopRunner, EventLoopRunnerShared}; +#[cfg(feature = "push-notifications")] +use windows::Networking::PushNotifications::PushNotificationChannel; type GetPointerFrameInfoHistory = unsafe extern "system" fn( pointerId: u32, @@ -144,6 +146,8 @@ pub(crate) struct PlatformSpecificEventLoopAttributes { pub(crate) dpi_aware: bool, pub(crate) msg_hook: Option bool + 'static>>, pub(crate) preferred_theme: Option, + #[cfg(feature = "push-notifications")] + pub(crate) push_channel: Option, } impl Default for PlatformSpecificEventLoopAttributes { @@ -153,6 +157,8 @@ impl Default for PlatformSpecificEventLoopAttributes { dpi_aware: true, msg_hook: None, preferred_theme: None, + #[cfg(feature = "push-notifications")] + push_channel: None, } } } @@ -163,6 +169,8 @@ pub struct EventLoopWindowTarget { thread_msg_target: HWND, pub(crate) preferred_theme: Arc>>, pub(crate) runner_shared: EventLoopRunnerShared, + #[cfg(feature = "push-notifications")] + pub(crate) push_channel: Arc>>, } impl EventLoop { @@ -203,6 +211,8 @@ impl EventLoop { thread_msg_target, runner_shared, preferred_theme: Arc::new(Mutex::new(attributes.preferred_theme)), + #[cfg(feature = "push-notifications")] + push_channel: Arc::new(Mutex::new(None)), }, _marker: PhantomData, }, @@ -338,6 +348,12 @@ impl EventLoopWindowTarget { let _ = unsafe { SendMessageW(window, *CHANGE_THEME_MSG_ID, WPARAM(0), LPARAM(0)) }; }); } + + #[cfg(feature = "push-notifications")] + #[inline] + pub fn set_push_channel(&self, channel: Option) { + *self.push_channel.lock() = channel; + } } fn main_thread_id() -> u32 { diff --git a/src/push.rs b/src/push.rs new file mode 100644 index 000000000..ae90ce96b --- /dev/null +++ b/src/push.rs @@ -0,0 +1,19 @@ +// Copyright 2021-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 + +//! Extends base context with remote Push Notifications registration support. +//! +//! On Apple platforms, when enabled, APNS registration is triggered at app startup. Once a push +//! token is obtained, it is emitted via an app event to Tauri. The same flow occurs if registration +//! fails, but with a different event. +//! +//! These types must remain generic to platform naming ("APNS," "FCM," etc.) to allow for future +//! expansion to other platforms. +//! + +/// The push token type. +pub type PushToken = Vec; + +/// Push notifications features and utilities. On most platforms, this consists of obtaining +/// the token (which may be triggered at app start), then exposing it to the developer. +pub trait PushNotifications {}