Bug 1861709 replace AudioCallbackDriver::ThreadRunning() assertions that mean to...
[gecko.git] / widget / windows / ToastNotificationHandler.cpp
blob2e3fe3ed27c659b16439d0f479868f36a77bcddb
1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim:set ts=2 sts=2 sw=2 et cin: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "ToastNotificationHandler.h"
9 #include <windows.foundation.h>
11 #include "gfxUtils.h"
12 #include "imgIContainer.h"
13 #include "imgIRequest.h"
14 #include "mozilla/gfx/2D.h"
15 #ifdef MOZ_BACKGROUNDTASKS
16 # include "mozilla/BackgroundTasks.h"
17 #endif
18 #include "mozilla/HashFunctions.h"
19 #include "mozilla/JSONStringWriteFuncs.h"
20 #include "mozilla/Result.h"
21 #include "mozilla/Logging.h"
22 #include "mozilla/Tokenizer.h"
23 #include "mozilla/Unused.h"
24 #include "mozilla/WindowsVersion.h"
25 #include "mozilla/intl/Localization.h"
26 #include "nsAppDirectoryServiceDefs.h"
27 #include "nsAppRunner.h"
28 #include "nsDirectoryServiceDefs.h"
29 #include "nsDirectoryServiceUtils.h"
30 #include "nsIDUtils.h"
31 #include "nsIStringBundle.h"
32 #include "nsIToolkitProfile.h"
33 #include "nsIToolkitProfileService.h"
34 #include "nsIURI.h"
35 #include "nsIWidget.h"
36 #include "nsIWindowMediator.h"
37 #include "nsNetUtil.h"
38 #include "nsPIDOMWindow.h"
39 #include "nsProxyRelease.h"
40 #include "nsXREDirProvider.h"
41 #include "ToastNotificationHeaderOnlyUtils.h"
42 #include "WidgetUtils.h"
43 #include "WinUtils.h"
45 #include "ToastNotification.h"
47 namespace mozilla {
48 namespace widget {
50 extern LazyLogModule sWASLog;
52 using namespace ABI::Windows::Data::Xml::Dom;
53 using namespace ABI::Windows::Foundation;
54 using namespace ABI::Windows::UI::Notifications;
55 using namespace Microsoft::WRL;
56 using namespace Microsoft::WRL::Wrappers;
57 using namespace toastnotification;
59 // Needed to disambiguate internal and Windows `ToastNotification` classes.
60 using WinToastNotification = ABI::Windows::UI::Notifications::ToastNotification;
61 using ToastActivationHandler =
62 ITypedEventHandler<WinToastNotification*, IInspectable*>;
63 using ToastDismissedHandler =
64 ITypedEventHandler<WinToastNotification*, ToastDismissedEventArgs*>;
65 using ToastFailedHandler =
66 ITypedEventHandler<WinToastNotification*, ToastFailedEventArgs*>;
67 using IVectorView_ToastNotification =
68 Collections::IVectorView<WinToastNotification*>;
70 NS_IMPL_ISUPPORTS(ToastNotificationHandler, nsIAlertNotificationImageListener)
72 static bool SetNodeValueString(const nsString& aString, IXmlNode* node,
73 IXmlDocument* xml) {
74 ComPtr<IXmlText> inputText;
75 HRESULT hr;
76 hr = xml->CreateTextNode(HStringReference(aString.get()).Get(), &inputText);
77 NS_ENSURE_TRUE(SUCCEEDED(hr), false);
79 ComPtr<IXmlNode> inputTextNode;
80 hr = inputText.As(&inputTextNode);
81 NS_ENSURE_TRUE(SUCCEEDED(hr), false);
83 ComPtr<IXmlNode> appendedChild;
84 hr = node->AppendChild(inputTextNode.Get(), &appendedChild);
85 NS_ENSURE_TRUE(SUCCEEDED(hr), false);
87 return true;
90 static bool SetAttribute(ComPtr<IXmlElement>& element,
91 const HStringReference& name, const nsAString& value) {
92 HString valueStr;
93 valueStr.Set(PromiseFlatString(value).get());
95 HRESULT hr = element->SetAttribute(name.Get(), valueStr.Get());
96 NS_ENSURE_TRUE(SUCCEEDED(hr), false);
98 return true;
101 static bool AddActionNode(ComPtr<IXmlDocument>& toastXml,
102 ComPtr<IXmlNode>& actionsNode,
103 const nsAString& actionTitle,
104 const nsAString& launchArg,
105 const nsAString& actionArgs,
106 const nsAString& actionPlacement = u""_ns,
107 const nsAString& activationType = u""_ns) {
108 ComPtr<IXmlElement> action;
109 HRESULT hr =
110 toastXml->CreateElement(HStringReference(L"action").Get(), &action);
111 NS_ENSURE_TRUE(SUCCEEDED(hr), false);
113 bool success =
114 SetAttribute(action, HStringReference(L"content"), actionTitle);
115 NS_ENSURE_TRUE(success, false);
117 // Action arguments overwrite the toast's launch arguments, so we need to
118 // prepend the launch arguments necessary for the Notification Server to
119 // reconstruct the toast's origin.
121 // Web Notification actions are arbitrary strings; to prevent breaking launch
122 // argument parsing the action argument must be last. All delimiters after
123 // `action` are part of the action arugment.
124 nsAutoString args = launchArg + u"\n"_ns +
125 nsDependentString(kLaunchArgAction) + u"\n"_ns +
126 actionArgs;
127 success = SetAttribute(action, HStringReference(L"arguments"), args);
128 NS_ENSURE_TRUE(success, false);
130 if (!actionPlacement.IsEmpty()) {
131 success =
132 SetAttribute(action, HStringReference(L"placement"), actionPlacement);
133 NS_ENSURE_TRUE(success, false);
136 if (!activationType.IsEmpty()) {
137 success = SetAttribute(action, HStringReference(L"activationType"),
138 activationType);
139 NS_ENSURE_TRUE(success, false);
141 // No special argument handling: when `activationType="system"`, `action` is
142 // a Windows-specific keyword, generally "dismiss" or "snooze".
143 success = SetAttribute(action, HStringReference(L"arguments"), actionArgs);
144 NS_ENSURE_TRUE(success, false);
147 // Add <action> to <actions>
148 ComPtr<IXmlNode> actionNode;
149 hr = action.As(&actionNode);
150 NS_ENSURE_TRUE(SUCCEEDED(hr), false);
152 ComPtr<IXmlNode> appendedChild;
153 hr = actionsNode->AppendChild(actionNode.Get(), &appendedChild);
154 NS_ENSURE_TRUE(SUCCEEDED(hr), false);
156 return true;
159 nsresult ToastNotificationHandler::GetWindowsTag(nsAString& aWindowsTag) {
160 aWindowsTag.Assign(mWindowsTag);
161 return NS_OK;
164 nsresult ToastNotificationHandler::SetWindowsTag(const nsAString& aWindowsTag) {
165 mWindowsTag.Assign(aWindowsTag);
166 return NS_OK;
169 // clang - format off
170 /* Populate the launch argument so the COM server can reconstruct the toast
171 * origin.
173 * program
174 * {MOZ_APP_NAME}
175 * profile
176 * {path to profile}
178 // clang-format on
179 Result<nsString, nsresult> ToastNotificationHandler::GetLaunchArgument() {
180 nsString launchArg;
182 // When the preference is false, the COM notification server will be invoked,
183 // discover that there is no `program`, and exit (successfully), after which
184 // Windows will invoke the in-product Windows 8-style callbacks. When true,
185 // the COM notification server will launch Firefox with sufficient arguments
186 // for Firefox to handle the notification.
187 if (!Preferences::GetBool(
188 "alerts.useSystemBackend.windows.notificationserver.enabled",
189 false)) {
190 // Include dummy key/value so that newline appended arguments aren't off by
191 // one line.
192 launchArg += u"invalid key\ninvalid value"_ns;
193 return launchArg;
196 // `program` argument.
197 launchArg += nsDependentString(kLaunchArgProgram) + u"\n"_ns MOZ_APP_NAME;
199 // `profile` argument.
200 nsCOMPtr<nsIFile> profDir;
201 bool wantCurrentProfile = true;
202 #ifdef MOZ_BACKGROUNDTASKS
203 if (BackgroundTasks::IsBackgroundTaskMode()) {
204 // Notifications popped from a background task want to invoke Firefox with a
205 // different profile -- the default browsing profile. We'd prefer to not
206 // specify a profile, so that the Firefox invoked by the notification server
207 // chooses its default profile, but this might pop the profile chooser in
208 // some configurations.
209 wantCurrentProfile = false;
211 nsCOMPtr<nsIToolkitProfileService> profileSvc =
212 do_GetService(NS_PROFILESERVICE_CONTRACTID);
213 if (profileSvc) {
214 nsCOMPtr<nsIToolkitProfile> defaultProfile;
215 nsresult rv =
216 profileSvc->GetDefaultProfile(getter_AddRefs(defaultProfile));
217 if (NS_SUCCEEDED(rv) && defaultProfile) {
218 // Not all installations have a default profile. But if one is set,
219 // then it should have a profile directory.
220 MOZ_TRY(defaultProfile->GetRootDir(getter_AddRefs(profDir)));
224 #endif
225 if (wantCurrentProfile) {
226 MOZ_TRY(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
227 getter_AddRefs(profDir)));
230 if (profDir) {
231 nsAutoString profilePath;
232 MOZ_TRY(profDir->GetPath(profilePath));
233 launchArg += u"\n"_ns + nsDependentString(kLaunchArgProfile) + u"\n"_ns +
234 profilePath;
237 // `windowsTag` argument.
238 launchArg +=
239 u"\n"_ns + nsDependentString(kLaunchArgTag) + u"\n"_ns + mWindowsTag;
241 // `logging` argument.
242 if (Preferences::GetBool(
243 "alerts.useSystemBackend.windows.notificationserver.verbose",
244 false)) {
245 // Signal notification to log verbose messages.
246 launchArg +=
247 u"\n"_ns + nsDependentString(kLaunchArgLogging) + u"\nverbose"_ns;
250 return launchArg;
253 static ComPtr<IToastNotificationManagerStatics>
254 GetToastNotificationManagerStatics() {
255 ComPtr<IToastNotificationManagerStatics> toastNotificationManagerStatics;
256 HRESULT hr = GetActivationFactory(
257 HStringReference(
258 RuntimeClass_Windows_UI_Notifications_ToastNotificationManager)
259 .Get(),
260 &toastNotificationManagerStatics);
261 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
263 return toastNotificationManagerStatics;
266 ToastNotificationHandler::~ToastNotificationHandler() {
267 if (mImageRequest) {
268 mImageRequest->Cancel(NS_BINDING_ABORTED);
269 mImageRequest = nullptr;
272 if (mHasImage && mImageFile) {
273 DebugOnly<nsresult> rv = mImageFile->Remove(false);
274 NS_ASSERTION(NS_SUCCEEDED(rv), "Cannot remove temporary image file");
277 UnregisterHandler();
280 void ToastNotificationHandler::UnregisterHandler() {
281 if (mNotification) {
282 mNotification->remove_Dismissed(mDismissedToken);
283 mNotification->remove_Activated(mActivatedToken);
284 mNotification->remove_Failed(mFailedToken);
287 mNotification = nullptr;
288 mNotifier = nullptr;
290 SendFinished();
293 nsresult ToastNotificationHandler::InitAlertAsync(
294 nsIAlertNotification* aAlert) {
295 MOZ_TRY(InitWindowsTag());
297 return aAlert->LoadImage(/* aTimeout = */ 0, this, /* aUserData = */ nullptr,
298 getter_AddRefs(mImageRequest));
301 // Uniquely identify this toast to Windows. Existing names and cookies are not
302 // suitable: we want something generated and unique. This is needed to check if
303 // toast is still present in the Windows Action Center when we receive a dismiss
304 // timeout.
306 // Local testing reveals that the space of tags is not global but instead is per
307 // AUMID. Since an installation uses a unique AUMID incorporating the install
308 // directory hash, it should not witness another installation's tag.
309 nsresult ToastNotificationHandler::InitWindowsTag() {
310 mWindowsTag.Truncate();
312 nsAutoString tag;
314 // Multiple profiles might overwrite each other's toast messages when a
315 // common name is used for a given host port. We prevent this by including
316 // the profile directory as part of the toast hash.
317 nsCOMPtr<nsIFile> profDir;
318 MOZ_TRY(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
319 getter_AddRefs(profDir)));
320 MOZ_TRY(profDir->GetPath(tag));
322 if (!mHostPort.IsEmpty()) {
323 // Notification originated from a web notification.
324 // `mName` will be in the form `{mHostPort}#tag:{tag}` if the notification
325 // was created with a tag and `{mHostPort}#notag:{uuid}` otherwise.
326 tag += mName;
327 } else {
328 // Notification originated from the browser chrome.
329 if (!mName.IsEmpty()) {
330 tag += u"chrome#tag:"_ns;
331 // Browser chrome notifications don't follow any convention for naming.
332 tag += mName;
333 } else {
334 // No associated name, append a UUID to prevent reuse of the same tag.
335 nsIDToCString uuidString(nsID::GenerateUUID());
336 size_t len = strlen(uuidString.get());
337 MOZ_ASSERT(len == NSID_LENGTH - 1);
338 nsAutoString uuid;
339 CopyASCIItoUTF16(nsDependentCSubstring(uuidString.get(), len), uuid);
341 tag += u"chrome#notag:"_ns;
342 tag += uuid;
346 // Windows notification tags are limited to 16 characters, or 64 characters
347 // after the Creators Update; therefore we hash the tag to fit the minimum
348 // range.
349 HashNumber hash = HashString(tag);
350 mWindowsTag.AppendPrintf("%010u", hash);
352 return NS_OK;
355 nsString ToastNotificationHandler::ActionArgsJSONString(
356 const nsString& aAction, const nsString& aOpaqueRelaunchData = u""_ns) {
357 nsAutoCString actionArgsData;
359 JSONStringRefWriteFunc js(actionArgsData);
360 JSONWriter w(js, JSONWriter::SingleLineStyle);
361 w.Start();
363 w.StringProperty("action", NS_ConvertUTF16toUTF8(aAction));
365 if (mIsSystemPrincipal) {
366 // Privileged/chrome alerts (not activated by Windows) can have custom
367 // relaunch data.
368 if (!aOpaqueRelaunchData.IsEmpty()) {
369 w.StringProperty("opaqueRelaunchData",
370 NS_ConvertUTF16toUTF8(aOpaqueRelaunchData));
373 // Privileged alerts include any provided name for metrics.
374 if (!mName.IsEmpty()) {
375 w.StringProperty("privilegedName", NS_ConvertUTF16toUTF8(mName));
377 } else {
378 if (!mHostPort.IsEmpty()) {
379 w.StringProperty("launchUrl", NS_ConvertUTF16toUTF8(mHostPort));
383 w.End();
385 return NS_ConvertUTF8toUTF16(actionArgsData);
388 ComPtr<IXmlDocument> ToastNotificationHandler::CreateToastXmlDocument() {
389 ComPtr<IToastNotificationManagerStatics> toastNotificationManagerStatics =
390 GetToastNotificationManagerStatics();
391 NS_ENSURE_TRUE(toastNotificationManagerStatics, nullptr);
393 ToastTemplateType toastTemplate;
394 if (mHostPort.IsEmpty()) {
395 toastTemplate =
396 mHasImage ? ToastTemplateType::ToastTemplateType_ToastImageAndText03
397 : ToastTemplateType::ToastTemplateType_ToastText03;
398 } else {
399 toastTemplate =
400 mHasImage ? ToastTemplateType::ToastTemplateType_ToastImageAndText04
401 : ToastTemplateType::ToastTemplateType_ToastText04;
404 ComPtr<IXmlDocument> toastXml;
405 toastNotificationManagerStatics->GetTemplateContent(toastTemplate, &toastXml);
407 if (!toastXml) {
408 return nullptr;
411 nsresult ns;
412 HRESULT hr;
413 bool success;
415 if (mHasImage) {
416 ComPtr<IXmlNodeList> toastImageElements;
417 hr = toastXml->GetElementsByTagName(HStringReference(L"image").Get(),
418 &toastImageElements);
419 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
421 ComPtr<IXmlNode> imageNode;
422 hr = toastImageElements->Item(0, &imageNode);
423 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
425 ComPtr<IXmlElement> image;
426 hr = imageNode.As(&image);
427 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
429 success = SetAttribute(image, HStringReference(L"src"), mImageUri);
430 NS_ENSURE_TRUE(success, nullptr);
433 ComPtr<IXmlNodeList> toastTextElements;
434 hr = toastXml->GetElementsByTagName(HStringReference(L"text").Get(),
435 &toastTextElements);
436 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
438 ComPtr<IXmlNode> titleTextNodeRoot;
439 hr = toastTextElements->Item(0, &titleTextNodeRoot);
440 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
442 ComPtr<IXmlNode> msgTextNodeRoot;
443 hr = toastTextElements->Item(1, &msgTextNodeRoot);
444 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
446 success = SetNodeValueString(mTitle, titleTextNodeRoot.Get(), toastXml.Get());
447 NS_ENSURE_TRUE(success, nullptr);
449 success = SetNodeValueString(mMsg, msgTextNodeRoot.Get(), toastXml.Get());
450 NS_ENSURE_TRUE(success, nullptr);
452 ComPtr<IXmlNodeList> toastElements;
453 hr = toastXml->GetElementsByTagName(HStringReference(L"toast").Get(),
454 &toastElements);
455 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
457 ComPtr<IXmlNode> toastNodeRoot;
458 hr = toastElements->Item(0, &toastNodeRoot);
459 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
461 ComPtr<IXmlElement> toastElement;
462 hr = toastNodeRoot.As(&toastElement);
463 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
465 if (mRequireInteraction) {
466 success = SetAttribute(toastElement, HStringReference(L"scenario"),
467 u"reminder"_ns);
468 NS_ENSURE_TRUE(success, nullptr);
471 auto maybeLaunchArg = GetLaunchArgument();
472 NS_ENSURE_TRUE(maybeLaunchArg.isOk(), nullptr);
473 nsString launchArg = maybeLaunchArg.unwrap();
475 nsString launchArgWithoutAction = launchArg;
477 if (!mIsSystemPrincipal) {
478 // Unprivileged/content alerts can't have custom relaunch data.
479 NS_WARNING_ASSERTION(mOpaqueRelaunchData.IsEmpty(),
480 "unprivileged/content alert "
481 "should have trivial `mOpaqueRelaunchData`");
484 launchArg += u"\n"_ns + nsDependentString(kLaunchArgAction) + u"\n"_ns +
485 ActionArgsJSONString(u""_ns, mOpaqueRelaunchData);
487 success = SetAttribute(toastElement, HStringReference(L"launch"), launchArg);
488 NS_ENSURE_TRUE(success, nullptr);
490 MOZ_LOG(sWASLog, LogLevel::Debug,
491 ("launchArg: '%s'", NS_ConvertUTF16toUTF8(launchArg).get()));
493 // Use newer toast layout, which makes images larger, for system
494 // (chrome-privileged) toasts.
495 if (mIsSystemPrincipal) {
496 ComPtr<IXmlNodeList> bindingElements;
497 hr = toastXml->GetElementsByTagName(HStringReference(L"binding").Get(),
498 &bindingElements);
499 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
501 ComPtr<IXmlNode> bindingNodeRoot;
502 hr = bindingElements->Item(0, &bindingNodeRoot);
503 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
505 ComPtr<IXmlElement> bindingElement;
506 hr = bindingNodeRoot.As(&bindingElement);
507 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
509 success = SetAttribute(bindingElement, HStringReference(L"template"),
510 u"ToastGeneric"_ns);
511 NS_ENSURE_TRUE(success, nullptr);
514 ComPtr<IXmlElement> actions;
515 hr = toastXml->CreateElement(HStringReference(L"actions").Get(), &actions);
516 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
518 ComPtr<IXmlNode> actionsNode;
519 hr = actions.As(&actionsNode);
520 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
522 nsCOMPtr<nsIStringBundleService> sbs =
523 do_GetService(NS_STRINGBUNDLE_CONTRACTID);
524 NS_ENSURE_TRUE(sbs, nullptr);
526 nsCOMPtr<nsIStringBundle> bundle;
527 sbs->CreateBundle("chrome://alerts/locale/alert.properties",
528 getter_AddRefs(bundle));
529 NS_ENSURE_TRUE(bundle, nullptr);
531 if (!mHostPort.IsEmpty()) {
532 AutoTArray<nsString, 1> formatStrings = {mHostPort};
534 ComPtr<IXmlNode> urlTextNodeRoot;
535 hr = toastTextElements->Item(2, &urlTextNodeRoot);
536 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
538 nsAutoString urlReference;
539 bundle->FormatStringFromName("source.label", formatStrings, urlReference);
541 success =
542 SetNodeValueString(urlReference, urlTextNodeRoot.Get(), toastXml.Get());
543 NS_ENSURE_TRUE(success, nullptr);
545 if (IsWin10AnniversaryUpdateOrLater()) {
546 ComPtr<IXmlElement> placementText;
547 hr = urlTextNodeRoot.As(&placementText);
548 if (SUCCEEDED(hr)) {
549 // placement is supported on Windows 10 Anniversary Update or later
550 SetAttribute(placementText, HStringReference(L"placement"),
551 u"attribution"_ns);
555 nsAutoString disableButtonTitle;
556 ns = bundle->FormatStringFromName("webActions.disableForOrigin.label",
557 formatStrings, disableButtonTitle);
558 NS_ENSURE_SUCCESS(ns, nullptr);
560 AddActionNode(toastXml, actionsNode, disableButtonTitle,
561 // TODO: launch into `about:preferences`?
562 launchArgWithoutAction, ActionArgsJSONString(u"snooze"_ns),
563 u"contextmenu"_ns);
566 bool wantSettings = true;
567 #ifdef MOZ_BACKGROUNDTASKS
568 if (BackgroundTasks::IsBackgroundTaskMode()) {
569 // Notifications popped from a background task want to invoke Firefox with a
570 // different profile -- the default browsing profile. Don't link to Firefox
571 // settings in some different profile: the relevant Firefox settings won't
572 // take effect.
573 wantSettings = false;
575 #endif
576 if (MOZ_LIKELY(wantSettings)) {
577 nsAutoString settingsButtonTitle;
578 bundle->GetStringFromName("webActions.settings.label", settingsButtonTitle);
579 success = AddActionNode(
580 toastXml, actionsNode, settingsButtonTitle, launchArgWithoutAction,
581 // TODO: launch into `about:preferences`?
582 ActionArgsJSONString(u"settings"_ns), u"contextmenu"_ns);
583 NS_ENSURE_TRUE(success, nullptr);
586 for (const auto& action : mActions) {
587 // Bug 1778596: include per-action icon from image URL.
588 nsString title;
589 ns = action->GetTitle(title);
590 NS_ENSURE_SUCCESS(ns, nullptr);
592 nsString actionString;
593 ns = action->GetAction(actionString);
594 NS_ENSURE_SUCCESS(ns, nullptr);
596 nsString opaqueRelaunchData;
597 ns = action->GetOpaqueRelaunchData(opaqueRelaunchData);
598 NS_ENSURE_SUCCESS(ns, nullptr);
600 MOZ_LOG(sWASLog, LogLevel::Debug,
601 ("launchArgWithoutAction for '%s': '%s'",
602 NS_ConvertUTF16toUTF8(actionString).get(),
603 NS_ConvertUTF16toUTF8(launchArgWithoutAction).get()));
605 // Privileged/chrome alerts can have actions that are activated by Windows.
606 // Recognize these actions and enable these activations.
607 bool activationType(false);
608 ns = action->GetWindowsSystemActivationType(&activationType);
609 NS_ENSURE_SUCCESS(ns, nullptr);
611 nsString activationTypeString(
612 (mIsSystemPrincipal && activationType) ? u"system"_ns : u""_ns);
614 nsString actionArgs;
615 if (mIsSystemPrincipal && activationType) {
616 // Privileged/chrome alerts that are activated by Windows can't have
617 // custom relaunch data.
618 actionArgs = actionString;
620 NS_WARNING_ASSERTION(opaqueRelaunchData.IsEmpty(),
621 "action with `windowsSystemActivationType=true` "
622 "should have trivial `opaqueRelaunchData`");
623 } else {
624 actionArgs = ActionArgsJSONString(actionString, opaqueRelaunchData);
627 success = AddActionNode(toastXml, actionsNode, title,
628 /* launchArg */ launchArgWithoutAction,
629 /* actionArgs */ actionArgs,
630 /* actionPlacement */ u""_ns,
631 /* activationType */ activationTypeString);
632 NS_ENSURE_TRUE(success, nullptr);
635 // Windows ignores scenario=reminder added by mRequiredInteraction if
636 // there's no non-contextmenu activationType=background action.
637 if (mRequireInteraction && !mActions.Length()) {
638 nsTArray<nsCString> resIds = {
639 "toolkit/global/alert.ftl"_ns,
641 RefPtr<intl::Localization> l10n = intl::Localization::Create(resIds, true);
642 IgnoredErrorResult rv;
643 nsAutoCString closeTitle;
644 l10n->FormatValueSync("notification-default-dismiss"_ns, {}, closeTitle,
645 rv);
646 NS_ENSURE_TRUE(!rv.Failed(), nullptr);
648 NS_ENSURE_TRUE(
649 AddActionNode(toastXml, actionsNode, NS_ConvertUTF8toUTF16(closeTitle),
650 launchArg, u""_ns, u""_ns, u"background"_ns),
651 nullptr);
654 ComPtr<IXmlNode> appendedChild;
655 hr = toastNodeRoot->AppendChild(actionsNode.Get(), &appendedChild);
656 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
658 if (mIsSilent) {
659 ComPtr<IXmlNode> audioNode;
660 // Create <audio silent="true"/> for silent notifications.
661 ComPtr<IXmlElement> audio;
662 hr = toastXml->CreateElement(HStringReference(L"audio").Get(), &audio);
663 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
665 SetAttribute(audio, HStringReference(L"silent"), u"true"_ns);
667 hr = audio.As(&audioNode);
668 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
669 hr = toastNodeRoot->AppendChild(audioNode.Get(), &appendedChild);
670 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
673 return toastXml;
676 nsresult ToastNotificationHandler::CreateToastXmlString(
677 const nsAString& aImageURL, nsAString& aString) {
678 HRESULT hr;
680 if (!aImageURL.IsEmpty()) {
681 // For testing: don't fetch and write image to disk, just include the URL.
682 mHasImage = true;
683 mImageUri.Assign(aImageURL);
686 ComPtr<IXmlDocument> toastXml = CreateToastXmlDocument();
687 if (!toastXml) {
688 return NS_ERROR_FAILURE;
691 ComPtr<IXmlNodeSerializer> ser;
692 hr = toastXml.As(&ser);
693 NS_ENSURE_TRUE(SUCCEEDED(hr), NS_ERROR_FAILURE);
695 HString data;
696 hr = ser->GetXml(data.GetAddressOf());
697 NS_ENSURE_TRUE(SUCCEEDED(hr), NS_ERROR_FAILURE);
699 uint32_t len = 0;
700 const wchar_t* rawData = data.GetRawBuffer(&len);
701 NS_ENSURE_TRUE(rawData, NS_ERROR_FAILURE);
702 aString.Assign(rawData, len);
704 return NS_OK;
707 bool ToastNotificationHandler::ShowAlert() {
708 if (!mBackend->IsActiveHandler(mName, this)) {
709 return false;
712 ComPtr<IXmlDocument> toastXml = CreateToastXmlDocument();
714 if (!toastXml) {
715 return false;
718 return CreateWindowsNotificationFromXml(toastXml);
721 bool ToastNotificationHandler::IsPrivate() { return mInPrivateBrowsing; }
723 void ToastNotificationHandler::HideAlert() {
724 if (mNotifier && mNotification) {
725 mNotifier->Hide(mNotification.Get());
729 bool ToastNotificationHandler::CreateWindowsNotificationFromXml(
730 ComPtr<IXmlDocument>& aXml) {
731 ComPtr<IToastNotificationFactory> factory;
732 HRESULT hr;
734 hr = GetActivationFactory(
735 HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotification)
736 .Get(),
737 &factory);
738 NS_ENSURE_TRUE(SUCCEEDED(hr), false);
740 hr = factory->CreateToastNotification(aXml.Get(), &mNotification);
741 NS_ENSURE_TRUE(SUCCEEDED(hr), false);
743 RefPtr<ToastNotificationHandler> self = this;
745 hr = mNotification->add_Activated(
746 Callback<ToastActivationHandler>([self](IToastNotification* aNotification,
747 IInspectable* aInspectable) {
748 return self->OnActivate(ComPtr<IToastNotification>(aNotification),
749 ComPtr<IInspectable>(aInspectable));
750 }).Get(),
751 &mActivatedToken);
752 NS_ENSURE_TRUE(SUCCEEDED(hr), false);
754 hr = mNotification->add_Dismissed(
755 Callback<ToastDismissedHandler>([self](IToastNotification* aNotification,
756 IToastDismissedEventArgs* aArgs) {
757 return self->OnDismiss(ComPtr<IToastNotification>(aNotification),
758 ComPtr<IToastDismissedEventArgs>(aArgs));
759 }).Get(),
760 &mDismissedToken);
761 NS_ENSURE_TRUE(SUCCEEDED(hr), false);
763 hr = mNotification->add_Failed(
764 Callback<ToastFailedHandler>([self](IToastNotification* aNotification,
765 IToastFailedEventArgs* aArgs) {
766 return self->OnFail(ComPtr<IToastNotification>(aNotification),
767 ComPtr<IToastFailedEventArgs>(aArgs));
768 }).Get(),
769 &mFailedToken);
770 NS_ENSURE_TRUE(SUCCEEDED(hr), false);
772 ComPtr<IToastNotification2> notification2;
773 hr = mNotification.As(&notification2);
774 NS_ENSURE_TRUE(SUCCEEDED(hr), false);
776 HString hTag;
777 hr = hTag.Set(mWindowsTag.get());
778 NS_ENSURE_TRUE(SUCCEEDED(hr), false);
780 hr = notification2->put_Tag(hTag.Get());
781 NS_ENSURE_TRUE(SUCCEEDED(hr), false);
783 ComPtr<IToastNotificationManagerStatics> toastNotificationManagerStatics =
784 GetToastNotificationManagerStatics();
785 NS_ENSURE_TRUE(toastNotificationManagerStatics, false);
787 HString aumid;
788 hr = aumid.Set(mAumid.get());
789 NS_ENSURE_TRUE(SUCCEEDED(hr), false);
790 hr = toastNotificationManagerStatics->CreateToastNotifierWithId(aumid.Get(),
791 &mNotifier);
792 NS_ENSURE_TRUE(SUCCEEDED(hr), false);
794 hr = mNotifier->Show(mNotification.Get());
795 NS_ENSURE_TRUE(SUCCEEDED(hr), false);
797 if (mAlertListener) {
798 mAlertListener->Observe(nullptr, "alertshow", mCookie.get());
801 return true;
804 void ToastNotificationHandler::SendFinished() {
805 if (!mSentFinished && mAlertListener) {
806 mAlertListener->Observe(nullptr, "alertfinished", mCookie.get());
809 mSentFinished = true;
812 HRESULT
813 ToastNotificationHandler::OnActivate(
814 const ComPtr<IToastNotification>& notification,
815 const ComPtr<IInspectable>& inspectable) {
816 MOZ_LOG(sWASLog, LogLevel::Info, ("OnActivate"));
818 if (mAlertListener) {
819 // Extract the `action` value from the argument string.
820 nsAutoString actionString;
821 if (inspectable) {
822 ComPtr<IToastActivatedEventArgs> eventArgs;
823 HRESULT hr = inspectable.As(&eventArgs);
824 if (SUCCEEDED(hr)) {
825 HString arguments;
826 hr = eventArgs->get_Arguments(arguments.GetAddressOf());
827 if (SUCCEEDED(hr)) {
828 uint32_t len = 0;
829 const char16_t* buffer = (char16_t*)arguments.GetRawBuffer(&len);
830 if (buffer) {
831 MOZ_LOG(sWASLog, LogLevel::Info,
832 ("OnActivate: arguments: %s",
833 NS_ConvertUTF16toUTF8(buffer).get()));
835 // Toast arguments are a newline separated key/value combination of
836 // launch arguments and an optional action argument provided as an
837 // argument to the toast's constructor. After the `action` key is
838 // found, the remainder of toast argument (including newlines) is
839 // the `action` value.
840 Tokenizer16 parse(buffer);
841 nsDependentSubstring token;
843 while (parse.ReadUntil(Tokenizer16::Token::NewLine(), token)) {
844 if (token == nsDependentString(kLaunchArgAction)) {
845 Unused << parse.ReadUntil(Tokenizer16::Token::EndOfFile(),
846 actionString);
847 } else {
848 // Next line is a value in a key/value pair, skip.
849 parse.SkipUntil(Tokenizer16::Token::NewLine());
851 // Skip newline.
852 Tokenizer16::Token unused;
853 Unused << parse.Next(unused);
860 // TODO: extract `action` from `actionString`, which is now JSON.
862 if (actionString.EqualsLiteral("settings")) {
863 mAlertListener->Observe(nullptr, "alertsettingscallback", mCookie.get());
864 } else if (actionString.EqualsLiteral("snooze")) {
865 mAlertListener->Observe(nullptr, "alertdisablecallback", mCookie.get());
866 } else if (mClickable) {
867 // When clicking toast, focus moves to another process, but we want to set
868 // focus on Firefox process.
869 nsCOMPtr<nsIWindowMediator> winMediator(
870 do_GetService(NS_WINDOWMEDIATOR_CONTRACTID));
871 if (winMediator) {
872 nsCOMPtr<mozIDOMWindowProxy> navWin;
873 winMediator->GetMostRecentWindow(u"navigator:browser",
874 getter_AddRefs(navWin));
875 if (navWin) {
876 nsCOMPtr<nsIWidget> widget =
877 WidgetUtils::DOMWindowToWidget(nsPIDOMWindowOuter::From(navWin));
878 if (widget) {
879 SetForegroundWindow(
880 static_cast<HWND>(widget->GetNativeData(NS_NATIVE_WINDOW)));
884 mAlertListener->Observe(nullptr, "alertclickcallback", mCookie.get());
887 mBackend->RemoveHandler(mName, this);
888 return S_OK;
891 // Returns `nullptr` if no such toast exists.
892 /* static */ ComPtr<IToastNotification>
893 ToastNotificationHandler::FindNotificationByTag(const nsAString& aWindowsTag,
894 const nsAString& aAumid) {
895 HRESULT hr = S_OK;
897 HString current_id;
898 current_id.Set(PromiseFlatString(aWindowsTag).get());
900 ComPtr<IToastNotificationManagerStatics> manager =
901 GetToastNotificationManagerStatics();
902 NS_ENSURE_TRUE(manager, nullptr);
904 ComPtr<IToastNotificationManagerStatics2> manager2;
905 hr = manager.As(&manager2);
906 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
908 ComPtr<IToastNotificationHistory> history;
909 hr = manager2->get_History(&history);
910 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
911 ComPtr<IToastNotificationHistory2> history2;
912 hr = history.As(&history2);
913 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
915 ComPtr<IVectorView_ToastNotification> toasts;
916 hr = history2->GetHistoryWithId(
917 HStringReference(PromiseFlatString(aAumid).get()).Get(), &toasts);
918 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
920 unsigned int hist_size;
921 hr = toasts->get_Size(&hist_size);
922 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
923 for (unsigned int i = 0; i < hist_size; i++) {
924 ComPtr<IToastNotification> hist_toast;
925 hr = toasts->GetAt(i, &hist_toast);
926 if (NS_WARN_IF(FAILED(hr))) {
927 continue;
930 ComPtr<IToastNotification2> hist_toast2;
931 hr = hist_toast.As(&hist_toast2);
932 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
934 HString history_id;
935 hr = hist_toast2->get_Tag(history_id.GetAddressOf());
936 NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
938 // We can not directly compare IToastNotification objects; their IUnknown
939 // pointers should be equivalent but under inspection were not. Therefore we
940 // use the notification's tag instead.
941 if (current_id == history_id) {
942 return hist_toast;
946 return nullptr;
949 // A single toast message can receive multiple dismiss events, at most one for
950 // the popup and at most one for the action center. We can't simply count
951 // dismiss events as the user may have disabled either popups or action center
952 // notifications, therefore we have to check if the toast remains in the history
953 // (action center) to determine if the toast is fully dismissed.
954 HRESULT
955 ToastNotificationHandler::OnDismiss(
956 const ComPtr<IToastNotification>& notification,
957 const ComPtr<IToastDismissedEventArgs>& aArgs) {
958 ComPtr<IToastNotification2> notification2;
959 HRESULT hr = notification.As(&notification2);
960 NS_ENSURE_TRUE(SUCCEEDED(hr), E_FAIL);
962 HString tagHString;
963 hr = notification2->get_Tag(tagHString.GetAddressOf());
964 NS_ENSURE_TRUE(SUCCEEDED(hr), E_FAIL);
966 unsigned int len;
967 const wchar_t* tagPtr = tagHString.GetRawBuffer(&len);
968 nsAutoString tag(tagPtr, len);
970 if (FindNotificationByTag(tag, mAumid)) {
971 return S_OK;
974 SendFinished();
975 mBackend->RemoveHandler(mName, this);
976 return S_OK;
979 HRESULT
980 ToastNotificationHandler::OnFail(const ComPtr<IToastNotification>& notification,
981 const ComPtr<IToastFailedEventArgs>& aArgs) {
982 HRESULT err;
983 aArgs->get_ErrorCode(&err);
984 MOZ_LOG(sWASLog, LogLevel::Error,
985 ("Error creating notification, error: %ld", err));
987 SendFinished();
988 mBackend->RemoveHandler(mName, this);
989 return S_OK;
992 nsresult ToastNotificationHandler::TryShowAlert() {
993 if (NS_WARN_IF(!ShowAlert())) {
994 mBackend->RemoveHandler(mName, this);
995 return NS_ERROR_FAILURE;
997 return NS_OK;
1000 NS_IMETHODIMP
1001 ToastNotificationHandler::OnImageMissing(nsISupports*) {
1002 return TryShowAlert();
1005 NS_IMETHODIMP
1006 ToastNotificationHandler::OnImageReady(nsISupports*, imgIRequest* aRequest) {
1007 nsresult rv = AsyncSaveImage(aRequest);
1008 if (NS_FAILED(rv)) {
1009 return TryShowAlert();
1011 return rv;
1014 nsresult ToastNotificationHandler::AsyncSaveImage(imgIRequest* aRequest) {
1015 nsresult rv =
1016 NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(mImageFile));
1017 NS_ENSURE_SUCCESS(rv, rv);
1019 rv = mImageFile->Append(u"notificationimages"_ns);
1020 NS_ENSURE_SUCCESS(rv, rv);
1022 rv = mImageFile->Create(nsIFile::DIRECTORY_TYPE, 0500);
1023 if (NS_FAILED(rv) && rv != NS_ERROR_FILE_ALREADY_EXISTS) {
1024 return rv;
1027 nsID uuid;
1028 rv = nsID::GenerateUUIDInPlace(uuid);
1029 NS_ENSURE_SUCCESS(rv, rv);
1031 NSID_TrimBracketsASCII uuidStr(uuid);
1032 uuidStr.AppendLiteral(".png");
1033 mImageFile->AppendNative(uuidStr);
1035 nsCOMPtr<imgIContainer> imgContainer;
1036 rv = aRequest->GetImage(getter_AddRefs(imgContainer));
1037 NS_ENSURE_SUCCESS(rv, rv);
1039 nsMainThreadPtrHandle<ToastNotificationHandler> self(
1040 new nsMainThreadPtrHolder<ToastNotificationHandler>(
1041 "ToastNotificationHandler", this));
1043 nsCOMPtr<nsIFile> imageFile(mImageFile);
1044 RefPtr<mozilla::gfx::SourceSurface> surface = imgContainer->GetFrame(
1045 imgIContainer::FRAME_FIRST,
1046 imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY);
1047 nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
1048 "ToastNotificationHandler::AsyncWriteImage",
1049 [self, imageFile, surface]() -> void {
1050 nsresult rv = NS_ERROR_FAILURE;
1051 if (surface) {
1052 FILE* file = nullptr;
1053 rv = imageFile->OpenANSIFileDesc("wb", &file);
1054 if (NS_SUCCEEDED(rv)) {
1055 rv = gfxUtils::EncodeSourceSurface(surface, ImageType::PNG, u""_ns,
1056 gfxUtils::eBinaryEncode, file);
1057 fclose(file);
1061 nsCOMPtr<nsIRunnable> cbRunnable = NS_NewRunnableFunction(
1062 "ToastNotificationHandler::AsyncWriteImageCb",
1063 [self, rv]() -> void {
1064 auto handler = const_cast<ToastNotificationHandler*>(self.get());
1065 handler->OnWriteImageFinished(rv);
1068 NS_DispatchToMainThread(cbRunnable);
1071 return mBackend->BackgroundDispatch(r);
1074 void ToastNotificationHandler::OnWriteImageFinished(nsresult rv) {
1075 if (NS_SUCCEEDED(rv)) {
1076 OnWriteImageSuccess();
1078 TryShowAlert();
1081 nsresult ToastNotificationHandler::OnWriteImageSuccess() {
1082 nsresult rv;
1084 nsCOMPtr<nsIURI> fileURI;
1085 rv = NS_NewFileURI(getter_AddRefs(fileURI), mImageFile);
1086 NS_ENSURE_SUCCESS(rv, rv);
1088 nsAutoCString uriStr;
1089 rv = fileURI->GetSpec(uriStr);
1090 NS_ENSURE_SUCCESS(rv, rv);
1092 AppendUTF8toUTF16(uriStr, mImageUri);
1094 mHasImage = true;
1096 return NS_OK;
1099 } // namespace widget
1100 } // namespace mozilla