From 26a029d407be480d791972afb5975cf62c9360a6 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 02:47:55 +0200 Subject: Adding upstream version 124.0.1. Signed-off-by: Daniel Baumann --- widget/windows/ToastNotificationHandler.cpp | 1167 +++++++++++++++++++++++++++ 1 file changed, 1167 insertions(+) create mode 100644 widget/windows/ToastNotificationHandler.cpp (limited to 'widget/windows/ToastNotificationHandler.cpp') diff --git a/widget/windows/ToastNotificationHandler.cpp b/widget/windows/ToastNotificationHandler.cpp new file mode 100644 index 0000000000..a493342719 --- /dev/null +++ b/widget/windows/ToastNotificationHandler.cpp @@ -0,0 +1,1167 @@ +/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sts=2 sw=2 et cin: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "ToastNotificationHandler.h" + +#include + +#include "gfxUtils.h" +#include "gfxPlatform.h" +#include "imgIContainer.h" +#include "imgIRequest.h" +#include "json/json.h" +#include "mozilla/gfx/2D.h" +#ifdef MOZ_BACKGROUNDTASKS +# include "mozilla/BackgroundTasks.h" +#endif +#include "mozilla/HashFunctions.h" +#include "mozilla/JSONStringWriteFuncs.h" +#include "mozilla/Result.h" +#include "mozilla/Logging.h" +#include "mozilla/Tokenizer.h" +#include "mozilla/Unused.h" +#include "mozilla/WindowsVersion.h" +#include "mozilla/intl/Localization.h" +#include "nsAppDirectoryServiceDefs.h" +#include "nsAppRunner.h" +#include "nsDirectoryServiceDefs.h" +#include "nsDirectoryServiceUtils.h" +#include "nsIDUtils.h" +#include "nsIStringBundle.h" +#include "nsIToolkitProfile.h" +#include "nsIToolkitProfileService.h" +#include "nsIURI.h" +#include "nsIWidget.h" +#include "nsIWindowMediator.h" +#include "nsNetUtil.h" +#include "nsPIDOMWindow.h" +#include "nsProxyRelease.h" +#include "nsXREDirProvider.h" +#include "ToastNotificationHeaderOnlyUtils.h" +#include "WidgetUtils.h" +#include "WinUtils.h" + +#include "ToastNotification.h" + +namespace mozilla { +namespace widget { + +extern LazyLogModule sWASLog; + +using namespace ABI::Windows::Data::Xml::Dom; +using namespace ABI::Windows::Foundation; +using namespace ABI::Windows::UI::Notifications; +using namespace Microsoft::WRL; +using namespace Microsoft::WRL::Wrappers; +using namespace toastnotification; + +// Needed to disambiguate internal and Windows `ToastNotification` classes. +using WinToastNotification = ABI::Windows::UI::Notifications::ToastNotification; +using ToastActivationHandler = + ITypedEventHandler; +using ToastDismissedHandler = + ITypedEventHandler; +using ToastFailedHandler = + ITypedEventHandler; +using IVectorView_ToastNotification = + Collections::IVectorView; + +NS_IMPL_ISUPPORTS(ToastNotificationHandler, nsIAlertNotificationImageListener) + +static bool SetNodeValueString(const nsString& aString, IXmlNode* node, + IXmlDocument* xml) { + ComPtr inputText; + HRESULT hr; + hr = xml->CreateTextNode(HStringReference(aString.get()).Get(), &inputText); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + ComPtr inputTextNode; + hr = inputText.As(&inputTextNode); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + ComPtr appendedChild; + hr = node->AppendChild(inputTextNode.Get(), &appendedChild); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + return true; +} + +static bool SetAttribute(ComPtr& element, + const HStringReference& name, const nsAString& value) { + HString valueStr; + valueStr.Set(PromiseFlatString(value).get()); + + HRESULT hr = element->SetAttribute(name.Get(), valueStr.Get()); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + return true; +} + +static bool AddActionNode(ComPtr& toastXml, + ComPtr& actionsNode, + const nsAString& actionTitle, + const nsAString& launchArg, + const nsAString& actionArgs, + const nsAString& actionPlacement = u""_ns, + const nsAString& activationType = u""_ns) { + ComPtr action; + HRESULT hr = + toastXml->CreateElement(HStringReference(L"action").Get(), &action); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + bool success = + SetAttribute(action, HStringReference(L"content"), actionTitle); + NS_ENSURE_TRUE(success, false); + + // Action arguments overwrite the toast's launch arguments, so we need to + // prepend the launch arguments necessary for the Notification Server to + // reconstruct the toast's origin. + // + // Web Notification actions are arbitrary strings; to prevent breaking launch + // argument parsing the action argument must be last. All delimiters after + // `action` are part of the action arugment. + nsAutoString args = launchArg + u"\n"_ns + + nsDependentString(kLaunchArgAction) + u"\n"_ns + + actionArgs; + success = SetAttribute(action, HStringReference(L"arguments"), args); + NS_ENSURE_TRUE(success, false); + + if (!actionPlacement.IsEmpty()) { + success = + SetAttribute(action, HStringReference(L"placement"), actionPlacement); + NS_ENSURE_TRUE(success, false); + } + + if (!activationType.IsEmpty()) { + success = SetAttribute(action, HStringReference(L"activationType"), + activationType); + NS_ENSURE_TRUE(success, false); + + // No special argument handling: when `activationType="system"`, `arguments` + // should be a Windows-specific keyword, namely "dismiss" or "snooze", which + // are supposed to make a system handled dismiss/snooze buttons. + // https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/adaptive-interactive-toasts?tabs=xml#snoozedismiss + // + // Note that while using it prevents calling our notification COM server, + // it somehow still calls OnActivate instead of OnDismiss. Thus, we still + // need to handle such callbacks manually by checking `arguments`. + success = SetAttribute(action, HStringReference(L"arguments"), actionArgs); + NS_ENSURE_TRUE(success, false); + } + + // Add to + ComPtr actionNode; + hr = action.As(&actionNode); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + ComPtr appendedChild; + hr = actionsNode->AppendChild(actionNode.Get(), &appendedChild); + NS_ENSURE_TRUE(SUCCEEDED(hr), false); + + return true; +} + +nsresult ToastNotificationHandler::GetWindowsTag(nsAString& aWindowsTag) { + aWindowsTag.Assign(mWindowsTag); + return NS_OK; +} + +nsresult ToastNotificationHandler::SetWindowsTag(const nsAString& aWindowsTag) { + mWindowsTag.Assign(aWindowsTag); + return NS_OK; +} + +// clang - format off +/* Populate the launch argument so the COM server can reconstruct the toast + * origin. + * + * program + * {MOZ_APP_NAME} + * profile + * {path to profile} + */ +// clang-format on +Result ToastNotificationHandler::GetLaunchArgument() { + nsString launchArg; + + // When the preference is false, the COM notification server will be invoked, + // discover that there is no `program`, and exit (successfully), after which + // Windows will invoke the in-product Windows 8-style callbacks. When true, + // the COM notification server will launch Firefox with sufficient arguments + // for Firefox to handle the notification. + if (!Preferences::GetBool( + "alerts.useSystemBackend.windows.notificationserver.enabled", + false)) { + // Include dummy key/value so that newline appended arguments aren't off by + // one line. + launchArg += u"invalid key\ninvalid value"_ns; + return launchArg; + } + + // `program` argument. + launchArg += nsDependentString(kLaunchArgProgram) + u"\n"_ns MOZ_APP_NAME; + + // `profile` argument. + nsCOMPtr profDir; + bool wantCurrentProfile = true; +#ifdef MOZ_BACKGROUNDTASKS + if (BackgroundTasks::IsBackgroundTaskMode()) { + // Notifications popped from a background task want to invoke Firefox with a + // different profile -- the default browsing profile. We'd prefer to not + // specify a profile, so that the Firefox invoked by the notification server + // chooses its default profile, but this might pop the profile chooser in + // some configurations. + wantCurrentProfile = false; + + nsCOMPtr profileSvc = + do_GetService(NS_PROFILESERVICE_CONTRACTID); + if (profileSvc) { + nsCOMPtr defaultProfile; + nsresult rv = + profileSvc->GetDefaultProfile(getter_AddRefs(defaultProfile)); + if (NS_SUCCEEDED(rv) && defaultProfile) { + // Not all installations have a default profile. But if one is set, + // then it should have a profile directory. + MOZ_TRY(defaultProfile->GetRootDir(getter_AddRefs(profDir))); + } + } + } +#endif + if (wantCurrentProfile) { + MOZ_TRY(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(profDir))); + } + + if (profDir) { + nsAutoString profilePath; + MOZ_TRY(profDir->GetPath(profilePath)); + launchArg += u"\n"_ns + nsDependentString(kLaunchArgProfile) + u"\n"_ns + + profilePath; + } + + // `windowsTag` argument. + launchArg += + u"\n"_ns + nsDependentString(kLaunchArgTag) + u"\n"_ns + mWindowsTag; + + // `logging` argument. + if (Preferences::GetBool( + "alerts.useSystemBackend.windows.notificationserver.verbose", + false)) { + // Signal notification to log verbose messages. + launchArg += + u"\n"_ns + nsDependentString(kLaunchArgLogging) + u"\nverbose"_ns; + } + + return launchArg; +} + +static ComPtr +GetToastNotificationManagerStatics() { + ComPtr toastNotificationManagerStatics; + HRESULT hr = GetActivationFactory( + HStringReference( + RuntimeClass_Windows_UI_Notifications_ToastNotificationManager) + .Get(), + &toastNotificationManagerStatics); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + return toastNotificationManagerStatics; +} + +ToastNotificationHandler::~ToastNotificationHandler() { + if (mImageRequest) { + mImageRequest->Cancel(NS_BINDING_ABORTED); + mImageRequest = nullptr; + } + + if (mHasImage && mImageFile) { + DebugOnly rv = mImageFile->Remove(false); + NS_ASSERTION(NS_SUCCEEDED(rv), "Cannot remove temporary image file"); + } + + UnregisterHandler(); +} + +void ToastNotificationHandler::UnregisterHandler() { + if (mNotification) { + mNotification->remove_Dismissed(mDismissedToken); + mNotification->remove_Activated(mActivatedToken); + mNotification->remove_Failed(mFailedToken); + } + + mNotification = nullptr; + mNotifier = nullptr; + + SendFinished(); +} + +nsresult ToastNotificationHandler::InitAlertAsync( + nsIAlertNotification* aAlert) { + MOZ_TRY(InitWindowsTag()); + +#ifdef MOZ_BACKGROUNDTASKS + nsAutoString imageUrl; + if (BackgroundTasks::IsBackgroundTaskMode() && + NS_SUCCEEDED(aAlert->GetImageURL(imageUrl)) && !imageUrl.IsEmpty()) { + // Bug 1870750: Image decoding relies on gfx and runs on a thread pool, + // which expects to have been initialized early and on the main thread. + // Since background tasks run headless this never occurs. In this case we + // force gfx initialization. + Unused << NS_WARN_IF(!gfxPlatform::GetPlatform()); + } +#endif + + return aAlert->LoadImage(/* aTimeout = */ 0, this, /* aUserData = */ nullptr, + getter_AddRefs(mImageRequest)); +} + +// Uniquely identify this toast to Windows. Existing names and cookies are not +// suitable: we want something generated and unique. This is needed to check if +// toast is still present in the Windows Action Center when we receive a dismiss +// timeout. +// +// Local testing reveals that the space of tags is not global but instead is per +// AUMID. Since an installation uses a unique AUMID incorporating the install +// directory hash, it should not witness another installation's tag. +nsresult ToastNotificationHandler::InitWindowsTag() { + mWindowsTag.Truncate(); + + nsAutoString tag; + + // Multiple profiles might overwrite each other's toast messages when a + // common name is used for a given host port. We prevent this by including + // the profile directory as part of the toast hash. + nsCOMPtr profDir; + MOZ_TRY(NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, + getter_AddRefs(profDir))); + MOZ_TRY(profDir->GetPath(tag)); + + if (!mHostPort.IsEmpty()) { + // Notification originated from a web notification. + // `mName` will be in the form `{mHostPort}#tag:{tag}` if the notification + // was created with a tag and `{mHostPort}#notag:{uuid}` otherwise. + tag += mName; + } else { + // Notification originated from the browser chrome. + if (!mName.IsEmpty()) { + tag += u"chrome#tag:"_ns; + // Browser chrome notifications don't follow any convention for naming. + tag += mName; + } else { + // No associated name, append a UUID to prevent reuse of the same tag. + nsIDToCString uuidString(nsID::GenerateUUID()); + size_t len = strlen(uuidString.get()); + MOZ_ASSERT(len == NSID_LENGTH - 1); + nsAutoString uuid; + CopyASCIItoUTF16(nsDependentCSubstring(uuidString.get(), len), uuid); + + tag += u"chrome#notag:"_ns; + tag += uuid; + } + } + + // Windows notification tags are limited to 16 characters, or 64 characters + // after the Creators Update; therefore we hash the tag to fit the minimum + // range. + HashNumber hash = HashString(tag); + mWindowsTag.AppendPrintf("%010u", hash); + + return NS_OK; +} + +nsString ToastNotificationHandler::ActionArgsJSONString( + const nsString& aAction, const nsString& aOpaqueRelaunchData = u""_ns) { + nsAutoCString actionArgsData; + + JSONStringRefWriteFunc js(actionArgsData); + JSONWriter w(js, JSONWriter::SingleLineStyle); + w.Start(); + + w.StringProperty("action", NS_ConvertUTF16toUTF8(aAction)); + + if (mIsSystemPrincipal) { + // Privileged/chrome alerts (not activated by Windows) can have custom + // relaunch data. + if (!aOpaqueRelaunchData.IsEmpty()) { + w.StringProperty("opaqueRelaunchData", + NS_ConvertUTF16toUTF8(aOpaqueRelaunchData)); + } + + // Privileged alerts include any provided name for metrics. + if (!mName.IsEmpty()) { + w.StringProperty("privilegedName", NS_ConvertUTF16toUTF8(mName)); + } + } else { + if (!mHostPort.IsEmpty()) { + w.StringProperty("launchUrl", NS_ConvertUTF16toUTF8(mHostPort)); + } + } + + w.End(); + + return NS_ConvertUTF8toUTF16(actionArgsData); +} + +ComPtr ToastNotificationHandler::CreateToastXmlDocument() { + ComPtr toastNotificationManagerStatics = + GetToastNotificationManagerStatics(); + NS_ENSURE_TRUE(toastNotificationManagerStatics, nullptr); + + ToastTemplateType toastTemplate; + if (mHostPort.IsEmpty()) { + toastTemplate = + mHasImage ? ToastTemplateType::ToastTemplateType_ToastImageAndText03 + : ToastTemplateType::ToastTemplateType_ToastText03; + } else { + toastTemplate = + mHasImage ? ToastTemplateType::ToastTemplateType_ToastImageAndText04 + : ToastTemplateType::ToastTemplateType_ToastText04; + } + + ComPtr toastXml; + toastNotificationManagerStatics->GetTemplateContent(toastTemplate, &toastXml); + + if (!toastXml) { + return nullptr; + } + + nsresult ns; + HRESULT hr; + bool success; + + if (mHasImage) { + ComPtr toastImageElements; + hr = toastXml->GetElementsByTagName(HStringReference(L"image").Get(), + &toastImageElements); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr imageNode; + hr = toastImageElements->Item(0, &imageNode); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr image; + hr = imageNode.As(&image); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + success = SetAttribute(image, HStringReference(L"src"), mImageUri); + NS_ENSURE_TRUE(success, nullptr); + + switch (mImagePlacement) { + case ImagePlacement::eHero: + success = + SetAttribute(image, HStringReference(L"placement"), u"hero"_ns); + NS_ENSURE_TRUE(success, nullptr); + break; + case ImagePlacement::eIcon: + success = SetAttribute(image, HStringReference(L"placement"), + u"appLogoOverride"_ns); + NS_ENSURE_TRUE(success, nullptr); + break; + case ImagePlacement::eInline: + // No attribute placement attribute for inline images. + break; + } + } + + ComPtr toastTextElements; + hr = toastXml->GetElementsByTagName(HStringReference(L"text").Get(), + &toastTextElements); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr titleTextNodeRoot; + hr = toastTextElements->Item(0, &titleTextNodeRoot); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr msgTextNodeRoot; + hr = toastTextElements->Item(1, &msgTextNodeRoot); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + success = SetNodeValueString(mTitle, titleTextNodeRoot.Get(), toastXml.Get()); + NS_ENSURE_TRUE(success, nullptr); + + success = SetNodeValueString(mMsg, msgTextNodeRoot.Get(), toastXml.Get()); + NS_ENSURE_TRUE(success, nullptr); + + ComPtr toastElements; + hr = toastXml->GetElementsByTagName(HStringReference(L"toast").Get(), + &toastElements); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr toastNodeRoot; + hr = toastElements->Item(0, &toastNodeRoot); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr toastElement; + hr = toastNodeRoot.As(&toastElement); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + if (mRequireInteraction) { + success = SetAttribute(toastElement, HStringReference(L"scenario"), + u"reminder"_ns); + NS_ENSURE_TRUE(success, nullptr); + } + + auto maybeLaunchArg = GetLaunchArgument(); + NS_ENSURE_TRUE(maybeLaunchArg.isOk(), nullptr); + nsString launchArg = maybeLaunchArg.unwrap(); + + nsString launchArgWithoutAction = launchArg; + + if (!mIsSystemPrincipal) { + // Unprivileged/content alerts can't have custom relaunch data. + NS_WARNING_ASSERTION(mOpaqueRelaunchData.IsEmpty(), + "unprivileged/content alert " + "should have trivial `mOpaqueRelaunchData`"); + } + + launchArg += u"\n"_ns + nsDependentString(kLaunchArgAction) + u"\n"_ns + + ActionArgsJSONString(u""_ns, mOpaqueRelaunchData); + + success = SetAttribute(toastElement, HStringReference(L"launch"), launchArg); + NS_ENSURE_TRUE(success, nullptr); + + MOZ_LOG(sWASLog, LogLevel::Debug, + ("launchArg: '%s'", NS_ConvertUTF16toUTF8(launchArg).get())); + + // Use newer toast layout for system (chrome-privileged) toasts. This gains us + // UI elements such as new image placement options (default image placement is + // larger and inline) and buttons. + if (mIsSystemPrincipal) { + ComPtr bindingElements; + hr = toastXml->GetElementsByTagName(HStringReference(L"binding").Get(), + &bindingElements); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr bindingNodeRoot; + hr = bindingElements->Item(0, &bindingNodeRoot); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr bindingElement; + hr = bindingNodeRoot.As(&bindingElement); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + success = SetAttribute(bindingElement, HStringReference(L"template"), + u"ToastGeneric"_ns); + NS_ENSURE_TRUE(success, nullptr); + } + + ComPtr actions; + hr = toastXml->CreateElement(HStringReference(L"actions").Get(), &actions); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + ComPtr actionsNode; + hr = actions.As(&actionsNode); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + nsCOMPtr sbs = + do_GetService(NS_STRINGBUNDLE_CONTRACTID); + NS_ENSURE_TRUE(sbs, nullptr); + + nsCOMPtr bundle; + sbs->CreateBundle("chrome://alerts/locale/alert.properties", + getter_AddRefs(bundle)); + NS_ENSURE_TRUE(bundle, nullptr); + + if (!mHostPort.IsEmpty()) { + AutoTArray formatStrings = {mHostPort}; + + ComPtr urlTextNodeRoot; + hr = toastTextElements->Item(2, &urlTextNodeRoot); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + nsAutoString urlReference; + bundle->FormatStringFromName("source.label", formatStrings, urlReference); + + success = + SetNodeValueString(urlReference, urlTextNodeRoot.Get(), toastXml.Get()); + NS_ENSURE_TRUE(success, nullptr); + + if (IsWin10AnniversaryUpdateOrLater()) { + ComPtr placementText; + hr = urlTextNodeRoot.As(&placementText); + if (SUCCEEDED(hr)) { + // placement is supported on Windows 10 Anniversary Update or later + SetAttribute(placementText, HStringReference(L"placement"), + u"attribution"_ns); + } + } + + nsAutoString disableButtonTitle; + ns = bundle->FormatStringFromName("webActions.disableForOrigin.label", + formatStrings, disableButtonTitle); + NS_ENSURE_SUCCESS(ns, nullptr); + + AddActionNode(toastXml, actionsNode, disableButtonTitle, + // TODO: launch into `about:preferences`? + launchArgWithoutAction, ActionArgsJSONString(u"snooze"_ns), + u"contextmenu"_ns); + } + + bool wantSettings = true; +#ifdef MOZ_BACKGROUNDTASKS + if (BackgroundTasks::IsBackgroundTaskMode()) { + // Notifications popped from a background task want to invoke Firefox with a + // different profile -- the default browsing profile. Don't link to Firefox + // settings in some different profile: the relevant Firefox settings won't + // take effect. + wantSettings = false; + } +#endif + if (MOZ_LIKELY(wantSettings)) { + nsAutoString settingsButtonTitle; + bundle->GetStringFromName("webActions.settings.label", settingsButtonTitle); + success = AddActionNode( + toastXml, actionsNode, settingsButtonTitle, launchArgWithoutAction, + // TODO: launch into `about:preferences`? + ActionArgsJSONString(u"settings"_ns), u"contextmenu"_ns); + NS_ENSURE_TRUE(success, nullptr); + } + + for (const auto& action : mActions) { + // Bug 1778596: include per-action icon from image URL. + nsString title; + ns = action->GetTitle(title); + NS_ENSURE_SUCCESS(ns, nullptr); + + nsString actionString; + ns = action->GetAction(actionString); + NS_ENSURE_SUCCESS(ns, nullptr); + + nsString opaqueRelaunchData; + ns = action->GetOpaqueRelaunchData(opaqueRelaunchData); + NS_ENSURE_SUCCESS(ns, nullptr); + + MOZ_LOG(sWASLog, LogLevel::Debug, + ("launchArgWithoutAction for '%s': '%s'", + NS_ConvertUTF16toUTF8(actionString).get(), + NS_ConvertUTF16toUTF8(launchArgWithoutAction).get())); + + // Privileged/chrome alerts can have actions that are activated by Windows. + // Recognize these actions and enable these activations. + bool activationType(false); + ns = action->GetWindowsSystemActivationType(&activationType); + NS_ENSURE_SUCCESS(ns, nullptr); + + nsString activationTypeString( + (mIsSystemPrincipal && activationType) ? u"system"_ns : u""_ns); + + nsString actionArgs; + if (mIsSystemPrincipal && activationType) { + // Privileged/chrome alerts that are activated by Windows can't have + // custom relaunch data. + actionArgs = actionString; + + NS_WARNING_ASSERTION(opaqueRelaunchData.IsEmpty(), + "action with `windowsSystemActivationType=true` " + "should have trivial `opaqueRelaunchData`"); + } else { + actionArgs = ActionArgsJSONString(actionString, opaqueRelaunchData); + } + + success = AddActionNode(toastXml, actionsNode, title, + /* launchArg */ launchArgWithoutAction, + /* actionArgs */ actionArgs, + /* actionPlacement */ u""_ns, + /* activationType */ activationTypeString); + NS_ENSURE_TRUE(success, nullptr); + } + + // Windows ignores scenario=reminder added by mRequiredInteraction if + // there's no non-contextmenu action. + if (mRequireInteraction && !mActions.Length()) { + // `activationType="system" arguments="dismiss" content=""` provides + // localized text from Windows, but we support more locales than Windows + // does, so let's have our own. + nsTArray resIds = { + "toolkit/global/alert.ftl"_ns, + }; + RefPtr l10n = intl::Localization::Create(resIds, true); + IgnoredErrorResult rv; + nsAutoCString closeTitle; + l10n->FormatValueSync("notification-default-dismiss"_ns, {}, closeTitle, + rv); + NS_ENSURE_TRUE(!rv.Failed(), nullptr); + + NS_ENSURE_TRUE( + AddActionNode(toastXml, actionsNode, NS_ConvertUTF8toUTF16(closeTitle), + u""_ns, u"dismiss"_ns, u""_ns, u"system"_ns), + nullptr); + } + + ComPtr appendedChild; + hr = toastNodeRoot->AppendChild(actionsNode.Get(), &appendedChild); + NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr); + + if (mIsSilent) { + ComPtr audioNode; + // Create