use std::collections::HashMap;
use std::sync::Mutex;
use ::windows::core::{IInspectable, Interface, HSTRING};
use ::windows::Data::Xml::Dom::XmlDocument;
use ::windows::Foundation::TypedEventHandler;
use ::windows::UI::Notifications::{NotificationSetting, ToastActivatedEventArgs, ToastFailedEventArgs, ToastNotification, ToastNotificationManager, ToastNotifier};
use ::windows::Win32::Foundation::ERROR_SUCCESS;
use ::windows::Win32::System::Registry::{RegCloseKey, RegOpenKeyExW, HKEY, HKEY_CURRENT_USER, KEY_READ};
use tauri::{AppHandle, Runtime};
use super::{toast, Clear, Payload, Request, Sink};
use crate::error::{Error, Result};
pub(super) struct Backend {
aumid: HSTRING,
sink: Sink,
shown: Mutex<HashMap<String, Shown>>,
}
struct Shown {
_toast: ToastNotification,
tag: HSTRING,
group: HSTRING,
conversation_id: Option<String>,
}
fn aumid_registered(identifier: &str) -> bool {
let key = HSTRING::from(format!("Software\\Classes\\AppUserModelId\\{identifier}"));
let mut handle = HKEY::default();
unsafe {
if RegOpenKeyExW(HKEY_CURRENT_USER, &key, None, KEY_READ, &mut handle) == ERROR_SUCCESS {
let _ = RegCloseKey(handle);
return true;
}
}
false
}
impl Backend {
pub fn new<R: Runtime>(_app: &AppHandle<R>, identifier: &str, sink: Sink) -> Self {
let exe_dir = std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.to_string_lossy().into_owned())).unwrap_or_default();
let dev = toast::is_dev_build(&exe_dir);
let aumid = toast::pick_aumid(identifier, aumid_registered(identifier), dev);
if aumid != identifier {
log::warn!("开发构建没有登记 AUMID,通知借用 PowerShell 的 AUMID 显示,点击可能回不到应用;用安装包安装后正常");
}
Self { aumid: HSTRING::from(aumid), sink, shown: Mutex::new(HashMap::new()) }
}
fn notifier(&self) -> ::windows::core::Result<ToastNotifier> {
ToastNotificationManager::CreateToastNotifierWithId(&self.aumid)
}
pub async fn show(&self, req: &Request, payload: &Payload) -> Result<()> {
self.show_now(req, payload)
}
fn show_now(&self, req: &Request, payload: &Payload) -> Result<()> {
let notifier = self.notifier().map_err(win_err)?;
if let Ok(setting) = notifier.Setting() {
if setting != NotificationSetting::Enabled {
return Err(Error::permission_denied("notification_disabled"));
}
}
let doc = XmlDocument::new().map_err(win_err)?;
doc.LoadXml(&HSTRING::from(toast::xml(req, payload))).map_err(win_err)?;
let toast_obj = ToastNotification::CreateToastNotification(&doc).map_err(win_err)?;
let tag = HSTRING::from(toast::tag(&req.id));
let group = HSTRING::from(toast::group(req.group_key()));
toast_obj.SetTag(&tag).map_err(win_err)?;
toast_obj.SetGroup(&group).map_err(win_err)?;
let sink = self.sink.clone();
toast_obj
.Activated(&TypedEventHandler::<ToastNotification, IInspectable>::new(move |_, args| {
let Some(args) = args.as_ref() else { return Ok(()) };
let Ok(args) = args.cast::<ToastActivatedEventArgs>() else { return Ok(()) };
if let Some(payload) = super::decode_arg(&args.Arguments()?.to_string()) {
sink(payload, false);
}
Ok(())
}))
.map_err(win_err)?;
let id = req.id.clone();
toast_obj
.Failed(&TypedEventHandler::<ToastNotification, ToastFailedEventArgs>::new(move |_, args| {
if let Some(args) = args.as_ref() {
log::warn!("通知 {id} 显示失败:{:?}", args.ErrorCode());
}
Ok(())
}))
.map_err(win_err)?;
notifier.Show(&toast_obj).map_err(win_err)?;
self.shown.lock().unwrap().insert(req.id.clone(), Shown { _toast: toast_obj, tag, group, conversation_id: req.conversation_id.clone() });
Ok(())
}
pub async fn clear(&self, target: &Clear) -> Result<()> {
let history = ToastNotificationManager::History().map_err(win_err)?;
let mut shown = self.shown.lock().unwrap();
match target {
Clear::Id(id) => {
if let Some(entry) = shown.remove(id) {
let _ = history.RemoveGroupedTagWithId(&entry.tag, &entry.group, &self.aumid);
}
}
Clear::Conversation(cid) => {
let ids: Vec<String> = shown.iter().filter(|(_, s)| s.conversation_id.as_deref() == Some(cid.as_str())).map(|(id, _)| id.clone()).collect();
for id in ids {
if let Some(entry) = shown.remove(&id) {
let _ = history.RemoveGroupedTagWithId(&entry.tag, &entry.group, &self.aumid);
}
}
let _ = history.RemoveGroupWithId(&HSTRING::from(toast::group(cid)), &self.aumid);
}
Clear::All => {
shown.clear();
history.ClearWithId(&self.aumid).map_err(win_err)?;
}
}
Ok(())
}
pub async fn permission(&self) -> &'static str {
match self.notifier().and_then(|n| n.Setting()) {
Ok(NotificationSetting::Enabled) => "granted",
Ok(_) => "denied",
Err(_) => "default",
}
}
pub async fn request_permission(&self) -> &'static str {
if self.permission().await == "denied" {
"denied"
} else {
"granted"
}
}
}
fn win_err(err: ::windows::core::Error) -> Error {
Error::internal(format!("Windows 通知:{err}"))
}