/* -*- 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/. */
// Needed to disambiguate internal and Windows `ToastNotification` classes. using WinToastNotification = ABI::Windows::UI::Notifications::ToastNotification; using ToastActivationHandler =
ITypedEventHandler<WinToastNotification*, IInspectable*>; using ToastDismissedHandler =
ITypedEventHandler<WinToastNotification*, ToastDismissedEventArgs*>; using ToastFailedHandler =
ITypedEventHandler<WinToastNotification*, ToastFailedEventArgs*>; using IVectorView_ToastNotification =
Collections::IVectorView<WinToastNotification*>;
// 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 (!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);
}
// 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<nsString, nsresult> 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;
}
// `profile` argument.
nsCOMPtr<nsIFile> 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<nsIToolkitProfileService> profileSvc =
do_GetService(NS_PROFILESERVICE_CONTRACTID); if (profileSvc) {
nsCOMPtr<nsIToolkitProfile> 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)));
}
#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
// 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<nsIFile> 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);
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));
}
}
// 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<IXmlNodeList> bindingElements;
hr = toastXml->GetElementsByTagName(HStringReference(L"binding").Get(),
&bindingElements);
NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
if (IsWin10AnniversaryUpdateOrLater()) {
ComPtr<IXmlElement> 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);
}
}
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 (constauto& action : mActions) { // Bug 1778596: include per-action icon from image URL.
nsString title;
ns = action->GetTitle(title);
NS_ENSURE_SUCCESS(ns, nullptr); if (!EnsureUTF16Validity(title)) {
MOZ_LOG(sWASLog, LogLevel::Warning,
("Notification text was invalid UTF16, unpaired surrogates have " "been replaced."));
}
nsString actionString;
ns = action->GetAction(actionString);
NS_ENSURE_SUCCESS(ns, nullptr); if (!EnsureUTF16Validity(actionString)) {
MOZ_LOG(sWASLog, LogLevel::Warning,
("Notification text was invalid UTF16, unpaired surrogates have " "been replaced."));
}
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 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);
}
// 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<nsCString> resIds = { "toolkit/global/alert.ftl"_ns,
};
RefPtr<intl::Localization> l10n = intl::Localization::Create(resIds, true);
IgnoredErrorResult rv;
nsAutoCString closeTitle;
l10n->FormatValueSync("notification-default-dismiss"_ns, {}, closeTitle,
rv);
NS_ENSURE_TRUE(!rv.Failed(), nullptr);
if (!aImageURL.IsEmpty()) { // For testing: don't fetch and write image to disk, just include the URL.
mHasImage = true;
mImageUri.Assign(aImageURL);
}
ComPtr<IXmlDocument> toastXml = CreateToastXmlDocument(); if (!toastXml) { return NS_ERROR_FAILURE;
}
if (mAlertListener) { // Extract the `action` value from the argument string.
nsAutoString argumentsString;
nsAutoString actionString; if (inspectable) {
ComPtr<IToastActivatedEventArgs> eventArgs;
HRESULT hr = inspectable.As(&eventArgs); if (SUCCEEDED(hr)) {
HString arguments;
hr = eventArgs->get_Arguments(arguments.GetAddressOf()); if (SUCCEEDED(hr)) {
uint32_t len = 0; const char16_t* buffer = (char16_t*)arguments.GetRawBuffer(&len); if (buffer) {
MOZ_LOG(sWASLog, LogLevel::Info,
("OnActivate: arguments: %s",
NS_ConvertUTF16toUTF8(buffer).get()));
argumentsString.Assign(buffer);
// Toast arguments are a newline separated key/value combination of // launch arguments and an optional action argument provided as an // argument to the toast's constructor. After the `action` key is // found, the remainder of toast argument (including newlines) is // the `action` value.
Tokenizer16 parse(buffer);
nsDependentSubstring token;
while (parse.ReadUntil(Tokenizer16::Token::NewLine(), token)) { if (token == nsDependentString(kLaunchArgAction)) {
Unused << parse.ReadUntil(Tokenizer16::Token::EndOfFile(),
actionString);
} else { // Next line is a value in a key/value pair, skip.
parse.SkipUntil(Tokenizer16::Token::NewLine());
} // Skip newline.
Tokenizer16::Token unused;
Unused << parse.Next(unused);
}
}
}
}
}
if (argumentsString.EqualsLiteral("dismiss")) { // XXX: Somehow Windows still fires OnActivate instead of OnDismiss for // supposedly system managed dismiss button (with activationType=system // and arguments=dismiss). We have to manually treat such callback as a // dismiss action. For this case `arguments` only includes a keyword so we // don't need to compare with a parsed result.
SendFinished();
} elseif (actionString.EqualsLiteral("settings")) {
mAlertListener->Observe(nullptr, "alertsettingscallback", mCookie.get());
} elseif (actionString.EqualsLiteral("snooze")) {
mAlertListener->Observe(nullptr, "alertdisablecallback", mCookie.get());
} elseif (mClickable) { // When clicking toast, focus moves to another process, but we want to set // focus on Firefox process.
nsCOMPtr<nsIWindowMediator> winMediator(
do_GetService(NS_WINDOWMEDIATOR_CONTRACTID)); if (winMediator) {
nsCOMPtr<mozIDOMWindowProxy> navWin;
winMediator->GetMostRecentBrowserWindow(getter_AddRefs(navWin)); if (navWin) {
nsCOMPtr<nsIWidget> widget =
WidgetUtils::DOMWindowToWidget(nsPIDOMWindowOuter::From(navWin)); if (widget) {
SetForegroundWindow( static_cast<HWND>(widget->GetNativeData(NS_NATIVE_WINDOW)));
}
}
}
if (mHandleActions) {
Json::Value jsonData;
Json::Reader jsonReader;
// We can not directly compare IToastNotification objects; their IUnknown // pointers should be equivalent but under inspection were not. Therefore we // use the notification's tag instead. if (current_id == history_id) { return hist_toast;
}
}
return nullptr;
}
// A single toast message can receive multiple dismiss events, at most one for // the popup and at most one for the action center. We can't simply count // dismiss events as the user may have disabled either popups or action center // notifications, therefore we have to check if the toast remains in the history // (action center) to determine if the toast is fully dismissed.
HRESULT
ToastNotificationHandler::OnDismiss( const ComPtr<IToastNotification>& notification, const ComPtr<IToastDismissedEventArgs>& aArgs) {
ComPtr<IToastNotification2> notification2;
HRESULT hr = notification.As(¬ification2);
NS_ENSURE_TRUE(SUCCEEDED(hr), E_FAIL);
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung ist noch experimentell.