notifications.cpp
1 #include "pch.h" 2 3 #include "notifications.h" 4 #include "utils/com_object_factory.h" 5 #include "utils/window.h" 6 7 #include <Unknwn.h> 8 #include <winrt/base.h> 9 #include <winrt/Windows.Foundation.h> 10 #include <winrt/Windows.Data.Xml.Dom.h> 11 #include <winrt/Windows.Foundation.Collections.h> 12 #include <winrt/Windows.UI.Notifications.h> 13 #include <winrt/Windows.UI.Notifications.Management.h> 14 #include <winrt/Windows.ApplicationModel.Background.h> 15 #include <winrt/Windows.System.h> 16 #include <winrt/Windows.System.UserProfile.h> 17 #include <wil/com.h> 18 #include <propvarutil.h> 19 #include <propkey.h> 20 #include <Shobjidl.h> 21 #include <filesystem> 22 23 #include <winerror.h> 24 #include <NotificationActivationCallback.h> 25 26 #include "BackgroundActivator/handler_functions.h" 27 28 using namespace winrt::Windows::ApplicationModel::Background; 29 using winrt::Windows::Data::Xml::Dom::XmlDocument; 30 using winrt::Windows::UI::Notifications::NotificationData; 31 using winrt::Windows::UI::Notifications::NotificationUpdateResult; 32 using winrt::Windows::UI::Notifications::ScheduledToastNotification; 33 using winrt::Windows::UI::Notifications::ToastNotification; 34 using winrt::Windows::UI::Notifications::ToastNotificationManager; 35 36 namespace fs = std::filesystem; 37 38 template<class... Ts> 39 struct overloaded : Ts... 40 { 41 using Ts::operator()...; 42 }; 43 44 template<class... Ts> 45 overloaded(Ts...) -> overloaded<Ts...>; 46 47 namespace // Strings in this namespace should not be localized 48 { 49 constexpr std::wstring_view TASK_NAME = L"PowerToysBackgroundNotificationsHandler"; 50 constexpr std::wstring_view TASK_ENTRYPOINT = L"BackgroundActivator.BackgroundHandler"; 51 constexpr std::wstring_view PACKAGED_APPLICATION_ID = L"PowerToys"; 52 53 std::wstring APPLICATION_ID = L"Microsoft.PowerToysWin32"; 54 constexpr std::wstring_view DEFAULT_TOAST_GROUP = L"PowerToysToastTag"; 55 } 56 57 static DWORD loop_thread_id() 58 { 59 static const DWORD thread_id = GetCurrentThreadId(); 60 return thread_id; 61 } 62 63 class DECLSPEC_UUID("DD5CACDA-7C2E-4997-A62A-04A597B58F76") NotificationActivator : public INotificationActivationCallback 64 { 65 public: 66 HRESULT __stdcall QueryInterface(_In_ REFIID iid, _Outptr_ void** resultInterface) override 67 { 68 static const QITAB qit[] = { 69 QITABENT(NotificationActivator, INotificationActivationCallback), 70 { 0 } 71 }; 72 return QISearch(this, qit, iid, resultInterface); 73 } 74 75 ULONG __stdcall AddRef() override 76 { 77 return ++_refCount; 78 } 79 80 ULONG __stdcall Release() override 81 { 82 LONG refCount = --_refCount; 83 if (refCount == 0) 84 { 85 PostThreadMessage(loop_thread_id(), WM_QUIT, 0, 0); 86 delete this; 87 } 88 return refCount; 89 } 90 91 virtual HRESULT STDMETHODCALLTYPE Activate( 92 LPCWSTR /*appUserModelId*/, 93 LPCWSTR invokedArgs, 94 const NOTIFICATION_USER_INPUT_DATA*, 95 ULONG) override 96 { 97 auto lib = LoadLibraryW(L"PowerToys.BackgroundActivatorDLL.dll"); 98 if (!lib) 99 { 100 return 1; 101 } 102 auto dispatcher = reinterpret_cast<decltype(dispatch_to_background_handler)*>(GetProcAddress(lib, "dispatch_to_background_handler")); 103 if (!dispatcher) 104 { 105 return 1; 106 } 107 108 dispatcher(invokedArgs); 109 110 return 0; 111 } 112 113 private: 114 std::atomic<long> _refCount; 115 }; 116 117 void notifications::run_desktop_app_activator_loop() 118 { 119 com_object_factory<NotificationActivator> factory; 120 121 (void)loop_thread_id(); 122 123 DWORD token; 124 auto res = CoRegisterClassObject(__uuidof(NotificationActivator), &factory, CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, &token); 125 if (!SUCCEEDED(res)) 126 { 127 return; 128 } 129 130 run_message_loop(); 131 CoRevokeClassObject(token); 132 } 133 134 void notifications::override_application_id(const std::wstring_view appID) 135 { 136 APPLICATION_ID = appID; 137 SetCurrentProcessExplicitAppUserModelID(APPLICATION_ID.c_str()); 138 } 139 140 void notifications::show_toast(std::wstring message, std::wstring title, toast_params params) 141 { 142 // The toast won't be actually activated in the background, since it doesn't have any buttons 143 show_toast_with_activations(std::move(message), std::move(title), {}, {}, std::move(params)); 144 } 145 146 constexpr inline void xml_escape(std::wstring data) 147 { 148 std::wstring buffer; 149 buffer.reserve(data.size()); 150 for (size_t pos = 0; pos != data.size(); ++pos) 151 { 152 switch (data[pos]) 153 { 154 case L'&': 155 buffer.append(L"&"); 156 break; 157 case L'\"': 158 buffer.append(L"""); 159 break; 160 case L'\'': 161 buffer.append(L"'"); 162 break; 163 case L'<': 164 buffer.append(L"<"); 165 break; 166 case L'>': 167 buffer.append(L">"); 168 break; 169 default: 170 buffer.append(&data[pos], 1); 171 break; 172 } 173 } 174 data.swap(buffer); 175 } 176 177 void notifications::show_toast_with_activations(std::wstring message, 178 std::wstring title, 179 std::wstring_view background_handler_id, 180 std::vector<action_t> actions, 181 toast_params params, 182 std::wstring launch_uri) 183 { 184 // DO NOT LOCALIZE any string in this function, because they're XML tags and a subject to 185 // https://learn.microsoft.com/windows/uwp/design/shell/tiles-and-notifications/toast-xml-schema 186 187 std::wstring toast_xml; 188 toast_xml.reserve(2048); 189 190 // We must set toast's title and contents immediately, because some of the toasts we send could be snoozed. 191 // Windows instantiates the snoozed toast from scratch before showing it again, so all bindings that were set 192 // using NotificationData would be empty. 193 // Add the launch attribute if launch_uri is provided; otherwise, omit it 194 toast_xml += LR"(<?xml version="1.0"?>)"; 195 if (!launch_uri.empty()) 196 { 197 toast_xml += LR"(<toast launch=")" + launch_uri + LR"(" activationType="protocol">)"; // Use the launch URI if provided 198 } 199 else 200 { 201 toast_xml += LR"(<toast>)"; // No launch attribute if empty 202 } 203 204 toast_xml += LR"(<visual><binding template="ToastGeneric">)"; 205 toast_xml += LR"(<text id="1">)"; 206 toast_xml += std::move(title); 207 toast_xml += LR"(</text>)"; 208 toast_xml += LR"(<text id="2">)"; 209 toast_xml += std::move(message); 210 toast_xml += LR"(</text>)"; 211 212 if (params.progress_bar.has_value()) 213 { 214 toast_xml += LR"(<progress title="{progressTitle}" value="{progressValue}" valueStringOverride="{progressValueString}" status="" />)"; 215 } 216 toast_xml += L"</binding></visual><actions>"; 217 for (size_t i = 0; i < size(actions); ++i) 218 { 219 std::visit(overloaded{ 220 [&](const snooze_button& b) { 221 const bool has_durations = !b.durations.empty() && size(b.durations) <= 5; 222 std::wstring selection_id = L"snoozeTime"; 223 selection_id += static_cast<wchar_t>(L'0' + i); 224 if (has_durations) 225 { 226 toast_xml += LR"(<input id=")"; 227 toast_xml += selection_id; 228 toast_xml += LR"(" type="selection" defaultInput=")"; 229 toast_xml += std::to_wstring(b.durations[0].minutes); 230 toast_xml += L'"'; 231 if (!b.snooze_title.empty()) 232 { 233 toast_xml += LR"( title=")"; 234 toast_xml += b.snooze_title; 235 toast_xml += L'"'; 236 } 237 toast_xml += L'>'; 238 for (const auto& duration : b.durations) 239 { 240 toast_xml += LR"(<selection id=")"; 241 toast_xml += std::to_wstring(duration.minutes); 242 toast_xml += LR"(" content=")"; 243 toast_xml += duration.label; 244 toast_xml += LR"("/>)"; 245 } 246 toast_xml += LR"(</input>)"; 247 } 248 }, 249 [](const auto&) {} }, 250 actions[i]); 251 } 252 253 for (size_t i = 0; i < size(actions); ++i) 254 { 255 std::visit(overloaded{ 256 [&](const link_button& b) { 257 toast_xml += LR"(<action activationType="protocol" )"; 258 if (b.context_menu) 259 { 260 toast_xml += LR"(placement="contextMenu" )"; 261 } 262 toast_xml += LR"(arguments=")"; 263 toast_xml += b.url; 264 toast_xml += LR"(" content=")"; 265 toast_xml += b.label; 266 toast_xml += LR"(" />)"; 267 }, 268 [&](const background_activated_button& b) { 269 toast_xml += LR"(<action activationType="background" )"; 270 if (b.context_menu) 271 { 272 toast_xml += LR"(placement="contextMenu" )"; 273 } 274 toast_xml += LR"(arguments=")"; 275 toast_xml += L"button_id=" + std::to_wstring(i); // pass the button ID 276 toast_xml += L"&handler="; 277 toast_xml += background_handler_id; 278 toast_xml += LR"(" content=")"; 279 toast_xml += b.label; 280 toast_xml += LR"(" />)"; 281 }, 282 [&](const snooze_button& b) { 283 const bool has_durations = !b.durations.empty() && size(b.durations) <= 5; 284 std::wstring selection_id = L"snoozeTime"; 285 selection_id += static_cast<wchar_t>(L'0' + i); 286 toast_xml += LR"(<action activationType="system" arguments="snooze" )"; 287 if (has_durations) 288 { 289 toast_xml += LR"(hint-inputId=")"; 290 toast_xml += selection_id; 291 toast_xml += '"'; 292 } 293 toast_xml += LR"( content=")"; 294 toast_xml += b.snooze_button_title; 295 toast_xml += LR"(" />)"; 296 } }, 297 actions[i]); 298 } 299 toast_xml += L"</actions></toast>"; 300 301 XmlDocument toast_xml_doc; 302 xml_escape(toast_xml); 303 toast_xml_doc.LoadXml(toast_xml); 304 ToastNotification notification{ toast_xml_doc }; 305 notification.Group(DEFAULT_TOAST_GROUP); 306 307 winrt::Windows::Foundation::Collections::StringMap map; 308 if (params.progress_bar.has_value()) 309 { 310 float progress = std::clamp(params.progress_bar->progress, 0.0f, 1.0f); 311 map.Insert(L"progressValue", std::to_wstring(progress)); 312 map.Insert(L"progressValueString", std::to_wstring(static_cast<int>(progress * 100)) + std::wstring(L"%")); 313 map.Insert(L"progressTitle", params.progress_bar->progress_title); 314 } 315 NotificationData data{ map }; 316 notification.Data(std::move(data)); 317 318 const auto notifier = 319 ToastNotificationManager::ToastNotificationManager::CreateToastNotifier(APPLICATION_ID); 320 321 // Set a tag-related params if it has a valid length 322 if (params.tag.has_value() && params.tag->length() < 64) 323 { 324 notification.Tag(*params.tag); 325 if (!params.resend_if_scheduled) 326 { 327 for (const auto& scheduled_toast : notifier.GetScheduledToastNotifications()) 328 { 329 if (scheduled_toast.Tag() == *params.tag) 330 { 331 return; 332 } 333 } 334 } 335 } 336 try 337 { 338 notifier.Show(notification); 339 } 340 catch (...) 341 { 342 } 343 } 344 345 void notifications::update_toast_progress_bar(std::wstring_view tag, progress_bar_params params) 346 { 347 const auto notifier = 348 ToastNotificationManager::ToastNotificationManager::CreateToastNotifier(APPLICATION_ID); 349 350 float progress = std::clamp(params.progress, 0.0f, 1.0f); 351 winrt::Windows::Foundation::Collections::StringMap map; 352 map.Insert(L"progressValue", std::to_wstring(progress)); 353 map.Insert(L"progressValueString", std::to_wstring(static_cast<int>(progress * 100)) + std::wstring(L"%")); 354 map.Insert(L"progressTitle", params.progress_title); 355 356 NotificationData data(map); 357 notifier.Update(data, tag, DEFAULT_TOAST_GROUP); 358 } 359 360 void notifications::remove_toasts_by_tag(std::wstring_view tag) 361 { 362 using namespace winrt::Windows::System; 363 try 364 { 365 User currentUser{ *User::FindAllAsync(UserType::LocalUser, UserAuthenticationStatus::LocallyAuthenticated).get().First() }; 366 if (!currentUser) 367 { 368 return; 369 } 370 currentUser.GetPropertyAsync(KnownUserProperties::AccountName()); 371 auto toastHistory = ToastNotificationManager::GetForUser(currentUser).History(); 372 373 toastHistory.Remove(tag, DEFAULT_TOAST_GROUP, APPLICATION_ID); 374 } 375 catch (...) 376 { 377 // Couldn't get the current user or problem removing the toast => nothing we can do 378 } 379 } 380 381 void notifications::remove_all_scheduled_toasts() 382 { 383 const auto notifier = ToastNotificationManager::ToastNotificationManager::CreateToastNotifier(APPLICATION_ID); 384 385 try 386 { 387 for (const auto& scheduled_toast : notifier.GetScheduledToastNotifications()) 388 { 389 notifier.RemoveFromSchedule(scheduled_toast); 390 } 391 } 392 catch (...) 393 { 394 } 395 }