/ src / common / notifications / notifications.cpp
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"&amp;");
156              break;
157          case L'\"':
158              buffer.append(L"&quot;");
159              break;
160          case L'\'':
161              buffer.append(L"&apos;");
162              break;
163          case L'<':
164              buffer.append(L"&lt;");
165              break;
166          case L'>':
167              buffer.append(L"&gt;");
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"&amp;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  }