From bde44fb4ceec9c7c93e1014ff21844197804f804 Mon Sep 17 00:00:00 2001
From: Cosmin Sabou
Date: Thu, 7 Mar 2024 03:08:10 +0200
Subject: [PATCH] Backed out 3 changesets (bug 1883476, bug 1826375) for
causing windows build bustages. CLOSED TREE
Backed out changeset bc8bdcfbcd9c (bug 1883476)
Backed out changeset 7d6333da6d31 (bug 1826375)
Backed out changeset f5f32253c79c (bug 1826375)
---
browser/app/profile/firefox.js | 3 +
browser/components/BrowserContentHandler.sys.mjs | 23 +
browser/components/tests/browser/browser.toml | 3 +
.../browser_system_notification_telemetry.js | 54 +
browser/config/mozconfigs/win32/mingwclang | 2 +-
browser/config/mozconfigs/win64/mingwclang | 2 +-
third_party/WinToast/LICENSE | 21 +
.../WinToast/moz-check-system-shortcut.patch | 81 ++
.../WinToast/moz-disable-create-shortcut.patch | 110 ++
third_party/WinToast/moz.yaml | 37 +
.../WinToast/upstream-add-toast-scenario.patch | 123 ++
third_party/WinToast/wintoastlib.cpp | 1197 ++++++++++++++++++++
third_party/WinToast/wintoastlib.h | 234 ++++
third_party/moz.build | 3 +
toolkit/content/license.html | 6 +
.../BackgroundTask_defaultagent.sys.mjs | 24 +-
toolkit/mozapps/defaultagent/DefaultAgent.cpp | 59 +
toolkit/mozapps/defaultagent/DefaultBrowser.cpp | 11 +
toolkit/mozapps/defaultagent/DefaultBrowser.h | 4 +
toolkit/mozapps/defaultagent/Notification.cpp | 596 +++++++++-
toolkit/mozapps/defaultagent/Notification.h | 7 +-
toolkit/mozapps/defaultagent/defaultagent.ini | 9 +
toolkit/mozapps/defaultagent/moz.build | 13 +
toolkit/mozapps/defaultagent/nsIDefaultAgent.idl | 14 +
24 files changed, 2626 insertions(+), 10 deletions(-)
create mode 100644 browser/components/tests/browser/browser_system_notification_telemetry.js
create mode 100644 third_party/WinToast/LICENSE
create mode 100644 third_party/WinToast/moz-check-system-shortcut.patch
create mode 100644 third_party/WinToast/moz-disable-create-shortcut.patch
create mode 100644 third_party/WinToast/moz.yaml
create mode 100644 third_party/WinToast/upstream-add-toast-scenario.patch
create mode 100644 third_party/WinToast/wintoastlib.cpp
create mode 100644 third_party/WinToast/wintoastlib.h
create mode 100644 toolkit/mozapps/defaultagent/defaultagent.ini
diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js
index 6fc98824e137..8e69ab1456b5 100644
--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -295,6 +295,9 @@ pref("browser.shell.checkDefaultPDF", true);
// Will be set to `true` if the user indicates that they don't want to be asked
// again about Firefox being their default PDF handler any more.
pref("browser.shell.checkDefaultPDF.silencedByUser", false);
+// URL to navigate to when launching Firefox after accepting the Windows Default
+// Browser Agent "Set Firefox as default" call to action.
+pref("browser.shell.defaultBrowserAgent.thanksURL", "https://www.mozilla.org/%LOCALE%/firefox/set-as-default/thanks/");
#endif
diff --git a/browser/components/BrowserContentHandler.sys.mjs b/browser/components/BrowserContentHandler.sys.mjs
index ca7cf4d2c456..d450aa493ab8 100644
--- a/browser/components/BrowserContentHandler.sys.mjs
+++ b/browser/components/BrowserContentHandler.sys.mjs
@@ -1457,6 +1457,29 @@ nsDefaultCommandLineHandler.prototype = {
console.error(e);
}
+ if (
+ AppConstants.platform == "win" &&
+ cmdLine.handleFlag("to-handle-default-browser-agent", false)
+ ) {
+ // The Default Browser Agent launches Firefox in response to a Windows
+ // native notification, but it does so in a non-standard manner.
+ Services.telemetry.setEventRecordingEnabled(
+ "browser.launched_to_handle",
+ true
+ );
+ Glean.browserLaunchedToHandle.systemNotification.record({
+ name: "default-browser-agent",
+ });
+
+ let thanksURI = Services.io.newURI(
+ Services.urlFormatter.formatURLPref(
+ "browser.shell.defaultBrowserAgent.thanksURL"
+ )
+ );
+ urilist.push(thanksURI);
+ principalList.push(lazy.gSystemPrincipal);
+ }
+
if (cmdLine.findFlag("screenshot", true) != -1) {
// Shouldn't have to push principal here with the screenshot flag
lazy.HeadlessShell.handleCmdLineArgs(
diff --git a/browser/components/tests/browser/browser.toml b/browser/components/tests/browser/browser.toml
index 0101461beef3..ffb3012e7259 100644
--- a/browser/components/tests/browser/browser.toml
+++ b/browser/components/tests/browser/browser.toml
@@ -33,5 +33,8 @@ skip-if = ["os == 'mac'"]
["browser_startup_homepage.js"]
+["browser_system_notification_telemetry.js"]
+run-if = ["os == 'win'"]
+
["browser_to_handle_telemetry.js"]
run-if = ["os == 'win'"]
diff --git a/browser/components/tests/browser/browser_system_notification_telemetry.js b/browser/components/tests/browser/browser_system_notification_telemetry.js
new file mode 100644
index 000000000000..6cc8d12165c6
--- /dev/null
+++ b/browser/components/tests/browser/browser_system_notification_telemetry.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+async function handleCommandLine(args, state) {
+ let newWinPromise;
+ let target = Services.urlFormatter.formatURLPref(
+ "browser.shell.defaultBrowserAgent.thanksURL"
+ );
+
+ const EXISTING_FILE = Cc["@mozilla.org/file/local;1"].createInstance(
+ Ci.nsIFile
+ );
+ EXISTING_FILE.initWithPath(getTestFilePath("dummy.pdf"));
+
+ if (state == Ci.nsICommandLine.STATE_INITIAL_LAUNCH) {
+ newWinPromise = BrowserTestUtils.waitForNewWindow({
+ url: target, // N.b.: trailing slashes matter when matching.
+ });
+ }
+
+ let cmdLineHandler = Cc["@mozilla.org/browser/final-clh;1"].getService(
+ Ci.nsICommandLineHandler
+ );
+
+ let fakeCmdLine = Cu.createCommandLine(args, EXISTING_FILE.parent, state);
+ cmdLineHandler.handle(fakeCmdLine);
+
+ if (newWinPromise) {
+ let newWin = await newWinPromise;
+ await BrowserTestUtils.closeWindow(newWin);
+ } else {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+}
+
+// Launching from the WDBA should open the "thanks" page and should send a
+// telemetry event.
+add_task(async function test_launched_to_handle_default_browser_agent() {
+ await handleCommandLine(
+ ["-to-handle-default-browser-agent"],
+ Ci.nsICommandLine.STATE_INITIAL_LAUNCH
+ );
+
+ TelemetryTestUtils.assertEvents(
+ [{ extra: { name: "default-browser-agent" } }],
+ {
+ category: "browser.launched_to_handle",
+ method: "system_notification",
+ object: "toast",
+ }
+ );
+});
diff --git a/browser/config/mozconfigs/win32/mingwclang b/browser/config/mozconfigs/win32/mingwclang
index befca5d114f2..00f74618f4d8 100644
--- a/browser/config/mozconfigs/win32/mingwclang
+++ b/browser/config/mozconfigs/win32/mingwclang
@@ -32,7 +32,7 @@ ac_add_options --enable-disk-remnant-avoidance
ac_add_options --disable-webrtc # Bug 1393901
ac_add_options --disable-geckodriver # Bug 1489320
ac_add_options --disable-update-agent # Bug 1561797
-ac_add_options --disable-default-browser-agent # Relies on toast notifications which don't build on mingw.
+ac_add_options --disable-default-browser-agent # WinToast does not build on mingw
ac_add_options --disable-notification-server # Toast notifications don't build on mingw.
# Find our toolchain
diff --git a/browser/config/mozconfigs/win64/mingwclang b/browser/config/mozconfigs/win64/mingwclang
index a99f6f5be0de..d80e4a7654a5 100644
--- a/browser/config/mozconfigs/win64/mingwclang
+++ b/browser/config/mozconfigs/win64/mingwclang
@@ -32,7 +32,7 @@ ac_add_options --enable-disk-remnant-avoidance
ac_add_options --disable-webrtc # Bug 1393901
ac_add_options --disable-geckodriver # Bug 1489320
ac_add_options --disable-update-agent # Bug 1561797
-ac_add_options --disable-default-browser-agent # Relies on toast notifications which don't build on mingw.
+ac_add_options --disable-default-browser-agent # WinToast does not build on mingw
ac_add_options --disable-notification-server # Toast notifications don't build on mingw.
# Find our toolchain
diff --git a/third_party/WinToast/LICENSE b/third_party/WinToast/LICENSE
new file mode 100644
index 000000000000..c3a4fb8868cb
--- /dev/null
+++ b/third_party/WinToast/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2016 Mohammed Boujemaoui Boulaghmoudi
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/third_party/WinToast/moz-check-system-shortcut.patch b/third_party/WinToast/moz-check-system-shortcut.patch
new file mode 100644
index 000000000000..84411ae7bc1b
--- /dev/null
+++ b/third_party/WinToast/moz-check-system-shortcut.patch
@@ -0,0 +1,81 @@
+diff --git a/src/wintoastlib.cpp b/src/wintoastlib.cpp
+index 0895ff7..ac8d5cf 100644
+--- a/src/wintoastlib.cpp
++++ b/src/wintoastlib.cpp
+@@ -213,8 +213,8 @@ namespace Util {
+ }
+
+
+- inline HRESULT defaultShellLinksDirectory(_In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) {
+- DWORD written = GetEnvironmentVariableW(L"APPDATA", path, nSize);
++ inline HRESULT commonShellLinksDirectory(_In_ const WCHAR* baseEnv, _In_ WCHAR* path, _In_ DWORD nSize) {
++ DWORD written = GetEnvironmentVariableW(baseEnv, path, nSize);
+ HRESULT hr = written > 0 ? S_OK : E_INVALIDARG;
+ if (SUCCEEDED(hr)) {
+ errno_t result = wcscat_s(path, nSize, DEFAULT_SHELL_LINKS_PATH);
+@@ -224,8 +224,8 @@ namespace Util {
+ return hr;
+ }
+
+- inline HRESULT defaultShellLinkPath(const std::wstring& appname, _In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) {
+- HRESULT hr = defaultShellLinksDirectory(path, nSize);
++ inline HRESULT commonShellLinkPath(_In_ const WCHAR* baseEnv, const std::wstring& appname, _In_ WCHAR* path, _In_ DWORD nSize) {
++ HRESULT hr = commonShellLinksDirectory(baseEnv, path, nSize);
+ if (SUCCEEDED(hr)) {
+ const std::wstring appLink(appname + DEFAULT_LINK_FORMAT);
+ errno_t result = wcscat_s(path, nSize, appLink.c_str());
+@@ -235,6 +235,13 @@ namespace Util {
+ return hr;
+ }
+
++ inline HRESULT defaultUserShellLinkPath(const std::wstring& appname, _In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) {
++ return commonShellLinkPath(L"APPDATA", appname, path, nSize);
++ }
++
++ inline HRESULT defaultSystemShellLinkPath(const std::wstring& appname, _In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) {
++ return commonShellLinkPath(L"PROGRAMDATA", appname, path, nSize);
++ }
+
+ inline PCWSTR AsString(ComPtr &xmlDocument) {
+ HSTRING xml;
+@@ -523,12 +530,19 @@ const std::wstring& WinToast::appUserModelId() const {
+
+ HRESULT WinToast::validateShellLinkHelper(_Out_ bool& wasChanged) {
+ WCHAR path[MAX_PATH] = { L'\0' };
+- Util::defaultShellLinkPath(_appName, path);
++ Util::defaultUserShellLinkPath(_appName, path);
+ // Check if the file exist
+ DWORD attr = GetFileAttributesW(path);
+ if (attr >= 0xFFFFFFF) {
+- DEBUG_MSG("Error, shell link not found. Try to create a new one in: " << path);
+- return E_FAIL;
++ // The shortcut may be in the system Start Menu.
++ WCHAR systemPath[MAX_PATH] = { L'\0' };
++ Util::defaultSystemShellLinkPath(_appName, systemPath);
++ attr = GetFileAttributesW(systemPath);
++ if (attr >= 0xFFFFFFF) {
++ DEBUG_MSG("Error, shell link not found. Try to create a new one in: " << path);
++ return E_FAIL;
++ }
++ wcscpy(path, systemPath);
+ }
+
+ // Let's load the file as shell link to validate.
+@@ -543,7 +557,7 @@ HRESULT WinToast::validateShellLinkHelper(_Out_ bool& wasChanged) {
+ ComPtr persistFile;
+ hr = shellLink.As(&persistFile);
+ if (SUCCEEDED(hr)) {
+- hr = persistFile->Load(path, STGM_READWRITE);
++ hr = persistFile->Load(path, STGM_READ);
+ if (SUCCEEDED(hr)) {
+ ComPtr propertyStore;
+ hr = shellLink.As(&propertyStore);
+@@ -583,7 +597,7 @@ HRESULT WinToast::validateShellLinkHelper(_Out_ bool& wasChanged) {
+ HRESULT WinToast::createShellLinkHelper() {
+ WCHAR exePath[MAX_PATH]{L'\0'};
+ WCHAR slPath[MAX_PATH]{L'\0'};
+- Util::defaultShellLinkPath(_appName, slPath);
++ Util::defaultUserShellLinkPath(_appName, slPath);
+ Util::defaultExecutablePath(exePath);
+ ComPtr shellLink;
+ HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink));
diff --git a/third_party/WinToast/moz-disable-create-shortcut.patch b/third_party/WinToast/moz-disable-create-shortcut.patch
new file mode 100644
index 000000000000..96ccac27320a
--- /dev/null
+++ b/third_party/WinToast/moz-disable-create-shortcut.patch
@@ -0,0 +1,110 @@
+diff --git a/src/wintoastlib.cpp b/src/wintoastlib.cpp
+index 0895ff7..52de554 100644
+--- a/src/wintoastlib.cpp
++++ b/src/wintoastlib.cpp
+@@ -391,6 +391,10 @@ void WinToast::setAppUserModelId(_In_ const std::wstring& aumi) {
+ DEBUG_MSG(L"Default App User Model Id: " << _aumi.c_str());
+ }
+
++void WinToast::setShortcutPolicy(_In_ ShortcutPolicy shortcutPolicy) {
++ _shortcutPolicy = shortcutPolicy;
++}
++
+ bool WinToast::isCompatible() {
+ DllImporter::initialize();
+ return !((DllImporter::SetCurrentProcessExplicitAppUserModelID == nullptr)
+@@ -492,10 +496,12 @@ bool WinToast::initialize(_Out_ WinToastError* error) {
+ return false;
+ }
+
+- if (createShortcut() < 0) {
+- setError(error, WinToastError::ShellLinkNotCreated);
+- DEBUG_MSG(L"Error while attaching the AUMI to the current proccess =(");
+- return false;
++ if (_shortcutPolicy != SHORTCUT_POLICY_IGNORE) {
++ if (createShortcut() < 0) {
++ setError(error, WinToastError::ShellLinkNotCreated);
++ DEBUG_MSG(L"Error while attaching the AUMI to the current proccess =(");
++ return false;
++ }
+ }
+
+ if (FAILED(DllImporter::SetCurrentProcessExplicitAppUserModelID(_aumi.c_str()))) {
+@@ -555,18 +561,23 @@ HRESULT WinToast::validateShellLinkHelper(_Out_ bool& wasChanged) {
+ hr = DllImporter::PropVariantToString(appIdPropVar, AUMI, MAX_PATH);
+ wasChanged = false;
+ if (FAILED(hr) || _aumi != AUMI) {
+- // AUMI Changed for the same app, let's update the current value! =)
+- wasChanged = true;
+- PropVariantClear(&appIdPropVar);
+- hr = InitPropVariantFromString(_aumi.c_str(), &appIdPropVar);
+- if (SUCCEEDED(hr)) {
+- hr = propertyStore->SetValue(PKEY_AppUserModel_ID, appIdPropVar);
++ if (_shortcutPolicy == SHORTCUT_POLICY_REQUIRE_CREATE) {
++ // AUMI Changed for the same app, let's update the current value! =)
++ wasChanged = true;
++ PropVariantClear(&appIdPropVar);
++ hr = InitPropVariantFromString(_aumi.c_str(), &appIdPropVar);
+ if (SUCCEEDED(hr)) {
+- hr = propertyStore->Commit();
+- if (SUCCEEDED(hr) && SUCCEEDED(persistFile->IsDirty())) {
+- hr = persistFile->Save(path, TRUE);
++ hr = propertyStore->SetValue(PKEY_AppUserModel_ID, appIdPropVar);
++ if (SUCCEEDED(hr)) {
++ hr = propertyStore->Commit();
++ if (SUCCEEDED(hr) && SUCCEEDED(persistFile->IsDirty())) {
++ hr = persistFile->Save(path, TRUE);
++ }
+ }
+ }
++ } else {
++ // Not allowed to touch the shortcut to fix the AUMI
++ hr = E_FAIL;
+ }
+ }
+ PropVariantClear(&appIdPropVar);
+@@ -581,6 +592,10 @@ HRESULT WinToast::validateShellLinkHelper(_Out_ bool& wasChanged) {
+
+
+ HRESULT WinToast::createShellLinkHelper() {
++ if (_shortcutPolicy != SHORTCUT_POLICY_REQUIRE_CREATE) {
++ return E_FAIL;
++ }
++
+ WCHAR exePath[MAX_PATH]{L'\0'};
+ WCHAR slPath[MAX_PATH]{L'\0'};
+ Util::defaultShellLinkPath(_appName, slPath);
+diff --git a/src/wintoastlib.h b/src/wintoastlib.h
+index 68b1cb1..dc8d745 100644
+--- a/src/wintoastlib.h
++++ b/src/wintoastlib.h
+@@ -173,6 +173,16 @@ namespace WinToastLib {
+ SHORTCUT_CREATE_FAILED = -4
+ };
+
++ enum ShortcutPolicy {
++ /* Don't check, create, or modify a shortcut. */
++ SHORTCUT_POLICY_IGNORE = 0,
++ /* Require a shortcut with matching AUMI, don't create or modify an existing one. */
++ SHORTCUT_POLICY_REQUIRE_NO_CREATE = 1,
++ /* Require a shortcut with matching AUMI, create if missing, modify if not matching.
++ * This is the default. */
++ SHORTCUT_POLICY_REQUIRE_CREATE = 2,
++ };
++
+ WinToast(void);
+ virtual ~WinToast();
+ static WinToast* instance();
+@@ -194,10 +204,12 @@ namespace WinToastLib {
+ const std::wstring& appUserModelId() const;
+ void setAppUserModelId(_In_ const std::wstring& aumi);
+ void setAppName(_In_ const std::wstring& appName);
++ void setShortcutPolicy(_In_ ShortcutPolicy policy);
+
+ protected:
+ bool _isInitialized{false};
+ bool _hasCoInitialized{false};
++ ShortcutPolicy _shortcutPolicy{SHORTCUT_POLICY_REQUIRE_CREATE};
+ std::wstring _appName{};
+ std::wstring _aumi{};
+ std::map> _buffer{};
diff --git a/third_party/WinToast/moz.yaml b/third_party/WinToast/moz.yaml
new file mode 100644
index 000000000000..7a27bf29ad3f
--- /dev/null
+++ b/third_party/WinToast/moz.yaml
@@ -0,0 +1,37 @@
+# Version of this schema
+schema: 1
+
+# Manual Update can be done with:
+# cd third_pary/WinToast
+# wget https://raw.githubusercontent.com/mohabouje/WinToast/master/src/wintoastlib.cpp
+# wget https://raw.githubusercontent.com/mohabouje/WinToast/master/src/wintoastlib.h
+# patch -p2 < moz-check-system-shortcut.patch
+# patch -p2 < moz-disable-create-shortcut.patch
+# patch -p2 < upstream-add-toast-scenario.patch
+
+bugzilla:
+ # Bugzilla product and component for this directory and subdirectories
+ product: Toolkit
+ component: "General"
+
+# Document the source of externally hosted code
+origin:
+
+ # Short name of the package/library
+ name: WinToast
+
+ description: WinToast is a lightly library written in C++ which brings a complete integration of the modern toast notifications of Windows 8 & Windows 10.
+
+ # Full URL for the package's homepage/etc
+ # Usually different from repository url
+ url: https://github.com/mohabouje/WinToast
+
+ # Human-readable identifier for this version/release
+ # Generally "version NNN", "tag SSS", "bookmark SSS"
+ release: commit 09227c72f16ccefc36e9d430dea3b435346dbcbc
+
+ # The package's license, where possible using the mnemonic from
+ # https://spdx.org/licenses/
+ # Multiple licenses can be specified (as a YAML list)
+ # A "LICENSE" file must exist containing the full license text
+ license: MIT
diff --git a/third_party/WinToast/upstream-add-toast-scenario.patch b/third_party/WinToast/upstream-add-toast-scenario.patch
new file mode 100644
index 000000000000..0be8bf878d69
--- /dev/null
+++ b/third_party/WinToast/upstream-add-toast-scenario.patch
@@ -0,0 +1,123 @@
+diff --git a/src/wintoastlib.cpp b/src/wintoastlib.cpp
+index 3cf5f21..1adfe19 100644
+--- a/src/wintoastlib.cpp
++++ b/src/wintoastlib.cpp
+@@ -677,6 +677,10 @@ INT64 WinToast::showToast(_In_ const WinToastTemplate& toast, _In_ IWinToastHan
+ (toast.duration() == WinToastTemplate::Duration::Short) ? L"short" : L"long");
+ }
+
++ if (SUCCEEDED(hr)) {
++ hr = addScenarioHelper(xmlDocument.Get(), toast.scenario());
++ }
++
+ } else {
+ DEBUG_MSG("Modern features (Actions/Sounds/Attributes) not supported in this os version");
+ }
+@@ -828,6 +832,28 @@ HRESULT WinToast::addDurationHelper(_In_ IXmlDocument *xml, _In_ const std::wstr
+ return hr;
+ }
+
++HRESULT WinToast::addScenarioHelper(_In_ IXmlDocument* xml, _In_ const std::wstring& scenario) {
++ ComPtr nodeList;
++ HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"toast").Get(), &nodeList);
++ if (SUCCEEDED(hr)) {
++ UINT32 length;
++ hr = nodeList->get_Length(&length);
++ if (SUCCEEDED(hr)) {
++ ComPtr toastNode;
++ hr = nodeList->Item(0, &toastNode);
++ if (SUCCEEDED(hr)) {
++ ComPtr toastElement;
++ hr = toastNode.As(&toastElement);
++ if (SUCCEEDED(hr)) {
++ hr = toastElement->SetAttribute(WinToastStringWrapper(L"scenario").Get(),
++ WinToastStringWrapper(scenario).Get());
++ }
++ }
++ }
++ }
++ return hr;
++}
++
+ HRESULT WinToast::setTextFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& text, _In_ UINT32 pos) {
+ ComPtr nodeList;
+ HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"text").Get(), &nodeList);
+@@ -1065,6 +1091,15 @@ void WinToastTemplate::setExpiration(_In_ INT64 millisecondsFromNow) {
+ _expiration = millisecondsFromNow;
+ }
+
++void WinToastLib::WinToastTemplate::setScenario(Scenario scenario) {
++ switch (scenario) {
++ case Scenario::Default: _scenario = L"Default"; break;
++ case Scenario::Alarm: _scenario = L"Alarm"; break;
++ case Scenario::IncomingCall: _scenario = L"IncomingCall"; break;
++ case Scenario::Reminder: _scenario = L"Reminder"; break;
++ }
++}
++
+ void WinToastTemplate::setAttributionText(_In_ const std::wstring& attributionText) {
+ _attributionText = attributionText;
+ }
+@@ -1112,6 +1147,10 @@ const std::wstring& WinToastTemplate::attributionText() const {
+ return _attributionText;
+ }
+
++const std::wstring& WinToastLib::WinToastTemplate::scenario() const {
++ return _scenario;
++}
++
+ INT64 WinToastTemplate::expiration() const {
+ return _expiration;
+ }
+diff --git a/src/wintoastlib.h b/src/wintoastlib.h
+index d028994..291e15f 100644
+--- a/src/wintoastlib.h
++++ b/src/wintoastlib.h
+@@ -63,6 +63,7 @@ namespace WinToastLib {
+
+ class WinToastTemplate {
+ public:
++ enum class Scenario { Default, Alarm, IncomingCall, Reminder };
+ enum Duration { System, Short, Long };
+ enum AudioOption { Default = 0, Silent, Loop };
+ enum TextField { FirstLine = 0, SecondLine, ThirdLine };
+@@ -114,13 +115,14 @@ namespace WinToastLib {
+ void setSecondLine(_In_ const std::wstring& text);
+ void setThirdLine(_In_ const std::wstring& text);
+ void setTextField(_In_ const std::wstring& txt, _In_ TextField pos);
+- void setAttributionText(_In_ const std::wstring & attributionText);
++ void setAttributionText(_In_ const std::wstring& attributionText);
+ void setImagePath(_In_ const std::wstring& imgPath);
+ void setAudioPath(_In_ WinToastTemplate::AudioSystemFile audio);
+ void setAudioPath(_In_ const std::wstring& audioPath);
+ void setAudioOption(_In_ WinToastTemplate::AudioOption audioOption);
+ void setDuration(_In_ Duration duration);
+ void setExpiration(_In_ INT64 millisecondsFromNow);
++ void setScenario(_In_ Scenario scenario);
+ void addAction(_In_ const std::wstring& label);
+
+ std::size_t textFieldsCount() const;
+@@ -132,6 +134,7 @@ namespace WinToastLib {
+ const std::wstring& imagePath() const;
+ const std::wstring& audioPath() const;
+ const std::wstring& attributionText() const;
++ const std::wstring& scenario() const;
+ INT64 expiration() const;
+ WinToastTemplateType type() const;
+ WinToastTemplate::AudioOption audioOption() const;
+@@ -142,6 +145,7 @@ namespace WinToastLib {
+ std::wstring _imagePath{};
+ std::wstring _audioPath{};
+ std::wstring _attributionText{};
++ std::wstring _scenario{L"Default"};
+ INT64 _expiration{0};
+ AudioOption _audioOption{WinToastTemplate::AudioOption::Default};
+ WinToastTemplateType _type{WinToastTemplateType::Text01};
+@@ -210,6 +214,7 @@ namespace WinToastLib {
+ HRESULT setAttributionTextFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& text);
+ HRESULT addActionHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& action, _In_ const std::wstring& arguments);
+ HRESULT addDurationHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& duration);
++ HRESULT addScenarioHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& scenario);
+ ComPtr notifier(_In_ bool* succeded) const;
+ void setError(_Out_opt_ WinToastError* error, _In_ WinToastError value);
+ };
diff --git a/third_party/WinToast/wintoastlib.cpp b/third_party/WinToast/wintoastlib.cpp
new file mode 100644
index 000000000000..ea5648a61d62
--- /dev/null
+++ b/third_party/WinToast/wintoastlib.cpp
@@ -0,0 +1,1197 @@
+/* * Copyright (C) 2016-2019 Mohammed Boujemaoui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ * the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#include "wintoastlib.h"
+#include
+#include
+#include
+#include
+
+#pragma comment(lib,"shlwapi")
+#pragma comment(lib,"user32")
+
+#ifdef NDEBUG
+ #define DEBUG_MSG(str) do { } while ( false )
+#else
+ #define DEBUG_MSG(str) do { std::wcout << str << std::endl; } while( false )
+#endif
+
+#define DEFAULT_SHELL_LINKS_PATH L"\\Microsoft\\Windows\\Start Menu\\Programs\\"
+#define DEFAULT_LINK_FORMAT L".lnk"
+#define STATUS_SUCCESS (0x00000000)
+
+
+// Quickstart: Handling toast activations from Win32 apps in Windows 10
+// https://blogs.msdn.microsoft.com/tiles_and_toasts/2015/10/16/quickstart-handling-toast-activations-from-win32-apps-in-windows-10/
+using namespace WinToastLib;
+namespace DllImporter {
+
+ // Function load a function from library
+ template
+ HRESULT loadFunctionFromLibrary(HINSTANCE library, LPCSTR name, Function &func) {
+ if (!library) {
+ return E_INVALIDARG;
+ }
+ func = reinterpret_cast(GetProcAddress(library, name));
+ return (func != nullptr) ? S_OK : E_FAIL;
+ }
+
+ typedef HRESULT(FAR STDAPICALLTYPE *f_SetCurrentProcessExplicitAppUserModelID)(__in PCWSTR AppID);
+ typedef HRESULT(FAR STDAPICALLTYPE *f_PropVariantToString)(_In_ REFPROPVARIANT propvar, _Out_writes_(cch) PWSTR psz, _In_ UINT cch);
+ typedef HRESULT(FAR STDAPICALLTYPE *f_RoGetActivationFactory)(_In_ HSTRING activatableClassId, _In_ REFIID iid, _COM_Outptr_ void ** factory);
+ typedef HRESULT(FAR STDAPICALLTYPE *f_WindowsCreateStringReference)(_In_reads_opt_(length + 1) PCWSTR sourceString, UINT32 length, _Out_ HSTRING_HEADER * hstringHeader, _Outptr_result_maybenull_ _Result_nullonfailure_ HSTRING * string);
+ typedef PCWSTR(FAR STDAPICALLTYPE *f_WindowsGetStringRawBuffer)(_In_ HSTRING string, _Out_ UINT32 *length);
+ typedef HRESULT(FAR STDAPICALLTYPE *f_WindowsDeleteString)(_In_opt_ HSTRING string);
+
+ static f_SetCurrentProcessExplicitAppUserModelID SetCurrentProcessExplicitAppUserModelID;
+ static f_PropVariantToString PropVariantToString;
+ static f_RoGetActivationFactory RoGetActivationFactory;
+ static f_WindowsCreateStringReference WindowsCreateStringReference;
+ static f_WindowsGetStringRawBuffer WindowsGetStringRawBuffer;
+ static f_WindowsDeleteString WindowsDeleteString;
+
+
+ template
+ _Check_return_ __inline HRESULT _1_GetActivationFactory(_In_ HSTRING activatableClassId, _COM_Outptr_ T** factory) {
+ return RoGetActivationFactory(activatableClassId, IID_INS_ARGS(factory));
+ }
+
+ template
+ inline HRESULT Wrap_GetActivationFactory(_In_ HSTRING activatableClassId, _Inout_ Details::ComPtrRef factory) noexcept {
+ return _1_GetActivationFactory(activatableClassId, factory.ReleaseAndGetAddressOf());
+ }
+
+ inline HRESULT initialize() {
+ HINSTANCE LibShell32 = LoadLibraryW(L"SHELL32.DLL");
+ HRESULT hr = loadFunctionFromLibrary(LibShell32, "SetCurrentProcessExplicitAppUserModelID", SetCurrentProcessExplicitAppUserModelID);
+ if (SUCCEEDED(hr)) {
+ HINSTANCE LibPropSys = LoadLibraryW(L"PROPSYS.DLL");
+ hr = loadFunctionFromLibrary(LibPropSys, "PropVariantToString", PropVariantToString);
+ if (SUCCEEDED(hr)) {
+ HINSTANCE LibComBase = LoadLibraryW(L"COMBASE.DLL");
+ const bool succeded = SUCCEEDED(loadFunctionFromLibrary(LibComBase, "RoGetActivationFactory", RoGetActivationFactory))
+ && SUCCEEDED(loadFunctionFromLibrary(LibComBase, "WindowsCreateStringReference", WindowsCreateStringReference))
+ && SUCCEEDED(loadFunctionFromLibrary(LibComBase, "WindowsGetStringRawBuffer", WindowsGetStringRawBuffer))
+ && SUCCEEDED(loadFunctionFromLibrary(LibComBase, "WindowsDeleteString", WindowsDeleteString));
+ return succeded ? S_OK : E_FAIL;
+ }
+ }
+ return hr;
+ }
+}
+
+class WinToastStringWrapper {
+public:
+ WinToastStringWrapper(_In_reads_(length) PCWSTR stringRef, _In_ UINT32 length) noexcept {
+ HRESULT hr = DllImporter::WindowsCreateStringReference(stringRef, length, &_header, &_hstring);
+ if (!SUCCEEDED(hr)) {
+ RaiseException(static_cast(STATUS_INVALID_PARAMETER), EXCEPTION_NONCONTINUABLE, 0, nullptr);
+ }
+ }
+
+ WinToastStringWrapper(_In_ const std::wstring &stringRef) noexcept {
+ HRESULT hr = DllImporter::WindowsCreateStringReference(stringRef.c_str(), static_cast(stringRef.length()), &_header, &_hstring);
+ if (FAILED(hr)) {
+ RaiseException(static_cast(STATUS_INVALID_PARAMETER), EXCEPTION_NONCONTINUABLE, 0, nullptr);
+ }
+ }
+
+ ~WinToastStringWrapper() {
+ DllImporter::WindowsDeleteString(_hstring);
+ }
+
+ inline HSTRING Get() const noexcept {
+ return _hstring;
+ }
+private:
+ HSTRING _hstring;
+ HSTRING_HEADER _header;
+
+};
+
+class InternalDateTime : public IReference {
+public:
+ static INT64 Now() {
+ FILETIME now;
+ GetSystemTimeAsFileTime(&now);
+ return ((((INT64)now.dwHighDateTime) << 32) | now.dwLowDateTime);
+ }
+
+ InternalDateTime(DateTime dateTime) : _dateTime(dateTime) {}
+
+ InternalDateTime(INT64 millisecondsFromNow) {
+ _dateTime.UniversalTime = Now() + millisecondsFromNow * 10000;
+ }
+
+ virtual ~InternalDateTime() = default;
+
+ operator INT64() {
+ return _dateTime.UniversalTime;
+ }
+
+ HRESULT STDMETHODCALLTYPE get_Value(DateTime *dateTime) {
+ *dateTime = _dateTime;
+ return S_OK;
+ }
+
+ HRESULT STDMETHODCALLTYPE QueryInterface(const IID& riid, void** ppvObject) {
+ if (!ppvObject) {
+ return E_POINTER;
+ }
+ if (riid == __uuidof(IUnknown) || riid == __uuidof(IReference)) {
+ *ppvObject = static_cast(static_cast*>(this));
+ return S_OK;
+ }
+ return E_NOINTERFACE;
+ }
+
+ ULONG STDMETHODCALLTYPE Release() {
+ return 1;
+ }
+
+ ULONG STDMETHODCALLTYPE AddRef() {
+ return 2;
+ }
+
+ HRESULT STDMETHODCALLTYPE GetIids(ULONG*, IID**) {
+ return E_NOTIMPL;
+ }
+
+ HRESULT STDMETHODCALLTYPE GetRuntimeClassName(HSTRING*) {
+ return E_NOTIMPL;
+ }
+
+ HRESULT STDMETHODCALLTYPE GetTrustLevel(TrustLevel*) {
+ return E_NOTIMPL;
+ }
+
+protected:
+ DateTime _dateTime;
+};
+
+namespace Util {
+
+ typedef LONG NTSTATUS, *PNTSTATUS;
+ typedef NTSTATUS(WINAPI* RtlGetVersionPtr)(PRTL_OSVERSIONINFOW);
+ inline RTL_OSVERSIONINFOW getRealOSVersion() {
+ HMODULE hMod = ::GetModuleHandleW(L"ntdll.dll");
+ if (hMod) {
+ RtlGetVersionPtr fxPtr = (RtlGetVersionPtr)::GetProcAddress(hMod, "RtlGetVersion");
+ if (fxPtr != nullptr) {
+ RTL_OSVERSIONINFOW rovi = { 0 };
+ rovi.dwOSVersionInfoSize = sizeof(rovi);
+ if (STATUS_SUCCESS == fxPtr(&rovi)) {
+ return rovi;
+ }
+ }
+ }
+ RTL_OSVERSIONINFOW rovi = { 0 };
+ return rovi;
+ }
+
+ inline HRESULT defaultExecutablePath(_In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) {
+ DWORD written = GetModuleFileNameExW(GetCurrentProcess(), nullptr, path, nSize);
+ DEBUG_MSG("Default executable path: " << path);
+ return (written > 0) ? S_OK : E_FAIL;
+ }
+
+
+ inline HRESULT commonShellLinksDirectory(_In_ const WCHAR* baseEnv, _In_ WCHAR* path, _In_ DWORD nSize) {
+ DWORD written = GetEnvironmentVariableW(baseEnv, path, nSize);
+ HRESULT hr = written > 0 ? S_OK : E_INVALIDARG;
+ if (SUCCEEDED(hr)) {
+ errno_t result = wcscat_s(path, nSize, DEFAULT_SHELL_LINKS_PATH);
+ hr = (result == 0) ? S_OK : E_INVALIDARG;
+ DEBUG_MSG("Default shell link path: " << path);
+ }
+ return hr;
+ }
+
+ inline HRESULT commonShellLinkPath(_In_ const WCHAR* baseEnv, const std::wstring& appname, _In_ WCHAR* path, _In_ DWORD nSize) {
+ HRESULT hr = commonShellLinksDirectory(baseEnv, path, nSize);
+ if (SUCCEEDED(hr)) {
+ const std::wstring appLink(appname + DEFAULT_LINK_FORMAT);
+ errno_t result = wcscat_s(path, nSize, appLink.c_str());
+ hr = (result == 0) ? S_OK : E_INVALIDARG;
+ DEBUG_MSG("Default shell link file path: " << path);
+ }
+ return hr;
+ }
+
+ inline HRESULT defaultUserShellLinkPath(const std::wstring& appname, _In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) {
+ return commonShellLinkPath(L"APPDATA", appname, path, nSize);
+ }
+
+ inline HRESULT defaultSystemShellLinkPath(const std::wstring& appname, _In_ WCHAR* path, _In_ DWORD nSize = MAX_PATH) {
+ return commonShellLinkPath(L"PROGRAMDATA", appname, path, nSize);
+ }
+
+ inline PCWSTR AsString(ComPtr &xmlDocument) {
+ HSTRING xml;
+ ComPtr ser;
+ HRESULT hr = xmlDocument.As(&ser);
+ hr = ser->GetXml(&xml);
+ if (SUCCEEDED(hr))
+ return DllImporter::WindowsGetStringRawBuffer(xml, nullptr);
+ return nullptr;
+ }
+
+ inline PCWSTR AsString(HSTRING hstring) {
+ return DllImporter::WindowsGetStringRawBuffer(hstring, nullptr);
+ }
+
+ inline HRESULT setNodeStringValue(const std::wstring& string, IXmlNode *node, IXmlDocument *xml) {
+ ComPtr textNode;
+ HRESULT hr = xml->CreateTextNode( WinToastStringWrapper(string).Get(), &textNode);
+ if (SUCCEEDED(hr)) {
+ ComPtr stringNode;
+ hr = textNode.As(&stringNode);
+ if (SUCCEEDED(hr)) {
+ ComPtr appendedChild;
+ hr = node->AppendChild(stringNode.Get(), &appendedChild);
+ }
+ }
+ return hr;
+ }
+
+ inline HRESULT setEventHandlers(_In_ IToastNotification* notification, _In_ std::shared_ptr eventHandler, _In_ INT64 expirationTime) {
+ EventRegistrationToken activatedToken, dismissedToken, failedToken;
+ HRESULT hr = notification->add_Activated(
+ Callback < Implements < RuntimeClassFlags,
+ ITypedEventHandler> >(
+ [eventHandler](IToastNotification*, IInspectable* inspectable)
+ {
+ IToastActivatedEventArgs *activatedEventArgs;
+ HRESULT hr = inspectable->QueryInterface(&activatedEventArgs);
+ if (SUCCEEDED(hr)) {
+ HSTRING argumentsHandle;
+ hr = activatedEventArgs->get_Arguments(&argumentsHandle);
+ if (SUCCEEDED(hr)) {
+ PCWSTR arguments = Util::AsString(argumentsHandle);
+ if (arguments && *arguments) {
+ eventHandler->toastActivated(static_cast(wcstol(arguments, nullptr, 10)));
+ return S_OK;
+ }
+ }
+ }
+ eventHandler->toastActivated();
+ return S_OK;
+ }).Get(), &activatedToken);
+
+ if (SUCCEEDED(hr)) {
+ hr = notification->add_Dismissed(Callback < Implements < RuntimeClassFlags,
+ ITypedEventHandler> >(
+ [eventHandler, expirationTime](IToastNotification*, IToastDismissedEventArgs* e)
+ {
+ ToastDismissalReason reason;
+ if (SUCCEEDED(e->get_Reason(&reason)))
+ {
+ if (reason == ToastDismissalReason_UserCanceled && expirationTime && InternalDateTime::Now() >= expirationTime)
+ reason = ToastDismissalReason_TimedOut;
+ eventHandler->toastDismissed(static_cast(reason));
+ }
+ return S_OK;
+ }).Get(), &dismissedToken);
+ if (SUCCEEDED(hr)) {
+ hr = notification->add_Failed(Callback < Implements < RuntimeClassFlags,
+ ITypedEventHandler> >(
+ [eventHandler](IToastNotification*, IToastFailedEventArgs*)
+ {
+ eventHandler->toastFailed();
+ return S_OK;
+ }).Get(), &failedToken);
+ }
+ }
+ return hr;
+ }
+
+ inline HRESULT addAttribute(_In_ IXmlDocument *xml, const std::wstring &name, IXmlNamedNodeMap *attributeMap) {
+ ComPtr srcAttribute;
+ HRESULT hr = xml->CreateAttribute(WinToastStringWrapper(name).Get(), &srcAttribute);
+ if (SUCCEEDED(hr)) {
+ ComPtr node;
+ hr = srcAttribute.As(&node);
+ if (SUCCEEDED(hr)) {
+ ComPtr pNode;
+ hr = attributeMap->SetNamedItem(node.Get(), &pNode);
+ }
+ }
+ return hr;
+ }
+
+ inline HRESULT createElement(_In_ IXmlDocument *xml, _In_ const std::wstring& root_node, _In_ const std::wstring& element_name, _In_ const std::vector& attribute_names) {
+ ComPtr rootList;
+ HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(root_node).Get(), &rootList);
+ if (SUCCEEDED(hr)) {
+ ComPtr root;
+ hr = rootList->Item(0, &root);
+ if (SUCCEEDED(hr)) {
+ ComPtr audioElement;
+ hr = xml->CreateElement(WinToastStringWrapper(element_name).Get(), &audioElement);
+ if (SUCCEEDED(hr)) {
+ ComPtr audioNodeTmp;
+ hr = audioElement.As(&audioNodeTmp);
+ if (SUCCEEDED(hr)) {
+ ComPtr audioNode;
+ hr = root->AppendChild(audioNodeTmp.Get(), &audioNode);
+ if (SUCCEEDED(hr)) {
+ ComPtr attributes;
+ hr = audioNode->get_Attributes(&attributes);
+ if (SUCCEEDED(hr)) {
+ for (const auto& it : attribute_names) {
+ hr = addAttribute(xml, it, attributes.Get());
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return hr;
+ }
+}
+
+WinToast* WinToast::instance() {
+ static WinToast instance;
+ return &instance;
+}
+
+WinToast::WinToast() :
+ _isInitialized(false),
+ _hasCoInitialized(false)
+{
+ if (!isCompatible()) {
+ DEBUG_MSG(L"Warning: Your system is not compatible with this library ");
+ }
+}
+
+WinToast::~WinToast() {
+ if (_hasCoInitialized) {
+ CoUninitialize();
+ }
+}
+
+void WinToast::setAppName(_In_ const std::wstring& appName) {
+ _appName = appName;
+}
+
+
+void WinToast::setAppUserModelId(_In_ const std::wstring& aumi) {
+ _aumi = aumi;
+ DEBUG_MSG(L"Default App User Model Id: " << _aumi.c_str());
+}
+
+void WinToast::setShortcutPolicy(_In_ ShortcutPolicy shortcutPolicy) {
+ _shortcutPolicy = shortcutPolicy;
+}
+
+bool WinToast::isCompatible() {
+ DllImporter::initialize();
+ return !((DllImporter::SetCurrentProcessExplicitAppUserModelID == nullptr)
+ || (DllImporter::PropVariantToString == nullptr)
+ || (DllImporter::RoGetActivationFactory == nullptr)
+ || (DllImporter::WindowsCreateStringReference == nullptr)
+ || (DllImporter::WindowsDeleteString == nullptr));
+}
+
+bool WinToastLib::WinToast::isSupportingModernFeatures() {
+ constexpr auto MinimumSupportedVersion = 6;
+ return Util::getRealOSVersion().dwMajorVersion > MinimumSupportedVersion;
+
+}
+std::wstring WinToast::configureAUMI(_In_ const std::wstring &companyName,
+ _In_ const std::wstring &productName,
+ _In_ const std::wstring &subProduct,
+ _In_ const std::wstring &versionInformation)
+{
+ std::wstring aumi = companyName;
+ aumi += L"." + productName;
+ if (subProduct.length() > 0) {
+ aumi += L"." + subProduct;
+ if (versionInformation.length() > 0) {
+ aumi += L"." + versionInformation;
+ }
+ }
+
+ if (aumi.length() > SCHAR_MAX) {
+ DEBUG_MSG("Error: max size allowed for AUMI: 128 characters.");
+ }
+ return aumi;
+}
+
+const std::wstring& WinToast::strerror(WinToastError error) {
+ static const std::unordered_map Labels = {
+ {WinToastError::NoError, L"No error. The process was executed correctly"},
+ {WinToastError::NotInitialized, L"The library has not been initialized"},
+ {WinToastError::SystemNotSupported, L"The OS does not support WinToast"},
+ {WinToastError::ShellLinkNotCreated, L"The library was not able to create a Shell Link for the app"},
+ {WinToastError::InvalidAppUserModelID, L"The AUMI is not a valid one"},
+ {WinToastError::InvalidParameters, L"The parameters used to configure the library are not valid normally because an invalid AUMI or App Name"},
+ {WinToastError::NotDisplayed, L"The toast was created correctly but WinToast was not able to display the toast"},
+ {WinToastError::UnknownError, L"Unknown error"}
+ };
+
+ const auto iter = Labels.find(error);
+ assert(iter != Labels.end());
+ return iter->second;
+}
+
+enum WinToast::ShortcutResult WinToast::createShortcut() {
+ if (_aumi.empty() || _appName.empty()) {
+ DEBUG_MSG(L"Error: App User Model Id or Appname is empty!");
+ return SHORTCUT_MISSING_PARAMETERS;
+ }
+
+ if (!isCompatible()) {
+ DEBUG_MSG(L"Your OS is not compatible with this library! =(");
+ return SHORTCUT_INCOMPATIBLE_OS;
+ }
+
+ if (!_hasCoInitialized) {
+ HRESULT initHr = CoInitializeEx(nullptr, COINIT::COINIT_MULTITHREADED);
+ if (initHr != RPC_E_CHANGED_MODE) {
+ if (FAILED(initHr) && initHr != S_FALSE) {
+ DEBUG_MSG(L"Error on COM library initialization!");
+ return SHORTCUT_COM_INIT_FAILURE;
+ }
+ else {
+ _hasCoInitialized = true;
+ }
+ }
+ }
+
+ bool wasChanged;
+ HRESULT hr = validateShellLinkHelper(wasChanged);
+ if (SUCCEEDED(hr))
+ return wasChanged ? SHORTCUT_WAS_CHANGED : SHORTCUT_UNCHANGED;
+
+ hr = createShellLinkHelper();
+ return SUCCEEDED(hr) ? SHORTCUT_WAS_CREATED : SHORTCUT_CREATE_FAILED;
+}
+
+bool WinToast::initialize(_Out_ WinToastError* error) {
+ _isInitialized = false;
+ setError(error, WinToastError::NoError);
+
+ if (!isCompatible()) {
+ setError(error, WinToastError::SystemNotSupported);
+ DEBUG_MSG(L"Error: system not supported.");
+ return false;
+ }
+
+
+ if (_aumi.empty() || _appName.empty()) {
+ setError(error, WinToastError::InvalidParameters);
+ DEBUG_MSG(L"Error while initializing, did you set up a valid AUMI and App name?");
+ return false;
+ }
+
+ if (_shortcutPolicy != SHORTCUT_POLICY_IGNORE) {
+ if (createShortcut() < 0) {
+ setError(error, WinToastError::ShellLinkNotCreated);
+ DEBUG_MSG(L"Error while attaching the AUMI to the current proccess =(");
+ return false;
+ }
+ }
+
+ if (FAILED(DllImporter::SetCurrentProcessExplicitAppUserModelID(_aumi.c_str()))) {
+ setError(error, WinToastError::InvalidAppUserModelID);
+ DEBUG_MSG(L"Error while attaching the AUMI to the current proccess =(");
+ return false;
+ }
+
+ _isInitialized = true;
+ return _isInitialized;
+}
+
+bool WinToast::isInitialized() const {
+ return _isInitialized;
+}
+
+const std::wstring& WinToast::appName() const {
+ return _appName;
+}
+
+const std::wstring& WinToast::appUserModelId() const {
+ return _aumi;
+}
+
+
+HRESULT WinToast::validateShellLinkHelper(_Out_ bool& wasChanged) {
+ WCHAR path[MAX_PATH] = { L'\0' };
+ Util::defaultUserShellLinkPath(_appName, path);
+ // Check if the file exist
+ DWORD attr = GetFileAttributesW(path);
+ if (attr >= 0xFFFFFFF) {
+ // The shortcut may be in the system Start Menu.
+ WCHAR systemPath[MAX_PATH] = { L'\0' };
+ Util::defaultSystemShellLinkPath(_appName, systemPath);
+ attr = GetFileAttributesW(systemPath);
+ if (attr >= 0xFFFFFFF) {
+ DEBUG_MSG("Error, shell link not found. Try to create a new one in: " << path);
+ return E_FAIL;
+ }
+ wcscpy(path, systemPath);
+ }
+
+ // Let's load the file as shell link to validate.
+ // - Create a shell link
+ // - Create a persistant file
+ // - Load the path as data for the persistant file
+ // - Read the property AUMI and validate with the current
+ // - Review if AUMI is equal.
+ ComPtr shellLink;
+ HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink));
+ if (SUCCEEDED(hr)) {
+ ComPtr persistFile;
+ hr = shellLink.As(&persistFile);
+ if (SUCCEEDED(hr)) {
+ hr = persistFile->Load(path, STGM_READ);
+ if (SUCCEEDED(hr)) {
+ ComPtr propertyStore;
+ hr = shellLink.As(&propertyStore);
+ if (SUCCEEDED(hr)) {
+ PROPVARIANT appIdPropVar;
+ hr = propertyStore->GetValue(PKEY_AppUserModel_ID, &appIdPropVar);
+ if (SUCCEEDED(hr)) {
+ WCHAR AUMI[MAX_PATH];
+ hr = DllImporter::PropVariantToString(appIdPropVar, AUMI, MAX_PATH);
+ wasChanged = false;
+ if (FAILED(hr) || _aumi != AUMI) {
+ if (_shortcutPolicy == SHORTCUT_POLICY_REQUIRE_CREATE) {
+ // AUMI Changed for the same app, let's update the current value! =)
+ wasChanged = true;
+ PropVariantClear(&appIdPropVar);
+ hr = InitPropVariantFromString(_aumi.c_str(), &appIdPropVar);
+ if (SUCCEEDED(hr)) {
+ hr = propertyStore->SetValue(PKEY_AppUserModel_ID, appIdPropVar);
+ if (SUCCEEDED(hr)) {
+ hr = propertyStore->Commit();
+ if (SUCCEEDED(hr) && SUCCEEDED(persistFile->IsDirty())) {
+ hr = persistFile->Save(path, TRUE);
+ }
+ }
+ }
+ } else {
+ // Not allowed to touch the shortcut to fix the AUMI
+ hr = E_FAIL;
+ }
+ }
+ PropVariantClear(&appIdPropVar);
+ }
+ }
+ }
+ }
+ }
+ return hr;
+}
+
+
+
+HRESULT WinToast::createShellLinkHelper() {
+ if (_shortcutPolicy != SHORTCUT_POLICY_REQUIRE_CREATE) {
+ return E_FAIL;
+ }
+
+ WCHAR exePath[MAX_PATH]{L'\0'};
+ WCHAR slPath[MAX_PATH]{L'\0'};
+ Util::defaultUserShellLinkPath(_appName, slPath);
+ Util::defaultExecutablePath(exePath);
+ ComPtr shellLink;
+ HRESULT hr = CoCreateInstance(CLSID_ShellLink, nullptr, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&shellLink));
+ if (SUCCEEDED(hr)) {
+ hr = shellLink->SetPath(exePath);
+ if (SUCCEEDED(hr)) {
+ hr = shellLink->SetArguments(L"");
+ if (SUCCEEDED(hr)) {
+ hr = shellLink->SetWorkingDirectory(exePath);
+ if (SUCCEEDED(hr)) {
+ ComPtr propertyStore;
+ hr = shellLink.As(&propertyStore);
+ if (SUCCEEDED(hr)) {
+ PROPVARIANT appIdPropVar;
+ hr = InitPropVariantFromString(_aumi.c_str(), &appIdPropVar);
+ if (SUCCEEDED(hr)) {
+ hr = propertyStore->SetValue(PKEY_AppUserModel_ID, appIdPropVar);
+ if (SUCCEEDED(hr)) {
+ hr = propertyStore->Commit();
+ if (SUCCEEDED(hr)) {
+ ComPtr persistFile;
+ hr = shellLink.As(&persistFile);
+ if (SUCCEEDED(hr)) {
+ hr = persistFile->Save(slPath, TRUE);
+ }
+ }
+ }
+ PropVariantClear(&appIdPropVar);
+ }
+ }
+ }
+ }
+ }
+ }
+ return hr;
+}
+
+INT64 WinToast::showToast(_In_ const WinToastTemplate& toast, _In_ IWinToastHandler* handler, _Out_ WinToastError* error) {
+ setError(error, WinToastError::NoError);
+ INT64 id = -1;
+ if (!isInitialized()) {
+ setError(error, WinToastError::NotInitialized);
+ DEBUG_MSG("Error when launching the toast. WinToast is not initialized.");
+ return id;
+ }
+ if (!handler) {
+ setError(error, WinToastError::InvalidHandler);
+ DEBUG_MSG("Error when launching the toast. Handler cannot be nullptr.");
+ return id;
+ }
+
+ ComPtr notificationManager;
+ HRESULT hr = DllImporter::Wrap_GetActivationFactory(WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), ¬ificationManager);
+ if (SUCCEEDED(hr)) {
+ ComPtr notifier;
+ hr = notificationManager->CreateToastNotifierWithId(WinToastStringWrapper(_aumi).Get(), ¬ifier);
+ if (SUCCEEDED(hr)) {
+ ComPtr notificationFactory;
+ hr = DllImporter::Wrap_GetActivationFactory(WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotification).Get(), ¬ificationFactory);
+ if (SUCCEEDED(hr)) {
+ ComPtr xmlDocument;
+ HRESULT hr = notificationManager->GetTemplateContent(ToastTemplateType(toast.type()), &xmlDocument);
+ if (SUCCEEDED(hr)) {
+ for (std::size_t i = 0, fieldsCount = toast.textFieldsCount(); i < fieldsCount && SUCCEEDED(hr); i++) {
+ hr = setTextFieldHelper(xmlDocument.Get(), toast.textField(WinToastTemplate::TextField(i)), i);
+ }
+
+ // Modern feature are supported Windows > Windows 10
+ if (SUCCEEDED(hr) && isSupportingModernFeatures()) {
+
+ // Note that we do this *after* using toast.textFieldsCount() to
+ // iterate/fill the template's text fields, since we're adding yet another text field.
+ if (SUCCEEDED(hr)
+ && !toast.attributionText().empty()) {
+ hr = setAttributionTextFieldHelper(xmlDocument.Get(), toast.attributionText());
+ }
+
+ std::array buf;
+ for (std::size_t i = 0, actionsCount = toast.actionsCount(); i < actionsCount && SUCCEEDED(hr); i++) {
+ _snwprintf_s(buf.data(), buf.size(), _TRUNCATE, L"%zd", i);
+ hr = addActionHelper(xmlDocument.Get(), toast.actionLabel(i), buf.data());
+ }
+
+ if (SUCCEEDED(hr)) {
+ hr = (toast.audioPath().empty() && toast.audioOption() == WinToastTemplate::AudioOption::Default)
+ ? hr : setAudioFieldHelper(xmlDocument.Get(), toast.audioPath(), toast.audioOption());
+ }
+
+ if (SUCCEEDED(hr) && toast.duration() != WinToastTemplate::Duration::System) {
+ hr = addDurationHelper(xmlDocument.Get(),
+ (toast.duration() == WinToastTemplate::Duration::Short) ? L"short" : L"long");
+ }
+
+ if (SUCCEEDED(hr)) {
+ hr = addScenarioHelper(xmlDocument.Get(), toast.scenario());
+ }
+
+ } else {
+ DEBUG_MSG("Modern features (Actions/Sounds/Attributes) not supported in this os version");
+ }
+
+ if (SUCCEEDED(hr)) {
+ hr = toast.hasImage() ? setImageFieldHelper(xmlDocument.Get(), toast.imagePath()) : hr;
+ if (SUCCEEDED(hr)) {
+ ComPtr notification;
+ hr = notificationFactory->CreateToastNotification(xmlDocument.Get(), ¬ification);
+ if (SUCCEEDED(hr)) {
+ INT64 expiration = 0, relativeExpiration = toast.expiration();
+ if (relativeExpiration > 0) {
+ InternalDateTime expirationDateTime(relativeExpiration);
+ expiration = expirationDateTime;
+ hr = notification->put_ExpirationTime(&expirationDateTime);
+ }
+
+ if (SUCCEEDED(hr)) {
+ hr = Util::setEventHandlers(notification.Get(), std::shared_ptr(handler), expiration);
+ if (FAILED(hr)) {
+ setError(error, WinToastError::InvalidHandler);
+ }
+ }
+
+ if (SUCCEEDED(hr)) {
+ GUID guid;
+ hr = CoCreateGuid(&guid);
+ if (SUCCEEDED(hr)) {
+ id = guid.Data1;
+ _buffer[id] = notification;
+ DEBUG_MSG("xml: " << Util::AsString(xmlDocument));
+ hr = notifier->Show(notification.Get());
+ if (FAILED(hr)) {
+ setError(error, WinToastError::NotDisplayed);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return FAILED(hr) ? -1 : id;
+}
+
+ComPtr WinToast::notifier(_In_ bool* succeded) const {
+ ComPtr notificationManager;
+ ComPtr notifier;
+ HRESULT hr = DllImporter::Wrap_GetActivationFactory(WinToastStringWrapper(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(), ¬ificationManager);
+ if (SUCCEEDED(hr)) {
+ hr = notificationManager->CreateToastNotifierWithId(WinToastStringWrapper(_aumi).Get(), ¬ifier);
+ }
+ *succeded = SUCCEEDED(hr);
+ return notifier;
+}
+
+bool WinToast::hideToast(_In_ INT64 id) {
+ if (!isInitialized()) {
+ DEBUG_MSG("Error when hiding the toast. WinToast is not initialized.");
+ return false;
+ }
+
+ if (_buffer.find(id) != _buffer.end()) {
+ auto succeded = false;
+ auto notify = notifier(&succeded);
+ if (succeded) {
+ auto result = notify->Hide(_buffer[id].Get());
+ _buffer.erase(id);
+ return SUCCEEDED(result);
+ }
+ }
+ return false;
+}
+
+void WinToast::clear() {
+ auto succeded = false;
+ auto notify = notifier(&succeded);
+ if (succeded) {
+ auto end = _buffer.end();
+ for (auto it = _buffer.begin(); it != end; ++it) {
+ notify->Hide(it->second.Get());
+ }
+ _buffer.clear();
+ }
+}
+
+//
+// Available as of Windows 10 Anniversary Update
+// Ref: https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/adaptive-interactive-toasts
+//
+// NOTE: This will add a new text field, so be aware when iterating over
+// the toast's text fields or getting a count of them.
+//
+HRESULT WinToast::setAttributionTextFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& text) {
+ Util::createElement(xml, L"binding", L"text", { L"placement" });
+ ComPtr nodeList;
+ HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"text").Get(), &nodeList);
+ if (SUCCEEDED(hr)) {
+ UINT32 nodeListLength;
+ hr = nodeList->get_Length(&nodeListLength);
+ if (SUCCEEDED(hr)) {
+ for (UINT32 i = 0; i < nodeListLength; i++) {
+ ComPtr textNode;
+ hr = nodeList->Item(i, &textNode);
+ if (SUCCEEDED(hr)) {
+ ComPtr attributes;
+ hr = textNode->get_Attributes(&attributes);
+ if (SUCCEEDED(hr)) {
+ ComPtr editedNode;
+ if (SUCCEEDED(hr)) {
+ hr = attributes->GetNamedItem(WinToastStringWrapper(L"placement").Get(), &editedNode);
+ if (FAILED(hr) || !editedNode) {
+ continue;
+ }
+ hr = Util::setNodeStringValue(L"attribution", editedNode.Get(), xml);
+ if (SUCCEEDED(hr)) {
+ return setTextFieldHelper(xml, text, i);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return hr;
+}
+
+HRESULT WinToast::addDurationHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& duration) {
+ ComPtr nodeList;
+ HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"toast").Get(), &nodeList);
+ if (SUCCEEDED(hr)) {
+ UINT32 length;
+ hr = nodeList->get_Length(&length);
+ if (SUCCEEDED(hr)) {
+ ComPtr toastNode;
+ hr = nodeList->Item(0, &toastNode);
+ if (SUCCEEDED(hr)) {
+ ComPtr toastElement;
+ hr = toastNode.As(&toastElement);
+ if (SUCCEEDED(hr)) {
+ hr = toastElement->SetAttribute(WinToastStringWrapper(L"duration").Get(),
+ WinToastStringWrapper(duration).Get());
+ }
+ }
+ }
+ }
+ return hr;
+}
+
+HRESULT WinToast::addScenarioHelper(_In_ IXmlDocument* xml, _In_ const std::wstring& scenario) {
+ ComPtr nodeList;
+ HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"toast").Get(), &nodeList);
+ if (SUCCEEDED(hr)) {
+ UINT32 length;
+ hr = nodeList->get_Length(&length);
+ if (SUCCEEDED(hr)) {
+ ComPtr toastNode;
+ hr = nodeList->Item(0, &toastNode);
+ if (SUCCEEDED(hr)) {
+ ComPtr toastElement;
+ hr = toastNode.As(&toastElement);
+ if (SUCCEEDED(hr)) {
+ hr = toastElement->SetAttribute(WinToastStringWrapper(L"scenario").Get(),
+ WinToastStringWrapper(scenario).Get());
+ }
+ }
+ }
+ }
+ return hr;
+}
+
+HRESULT WinToast::setTextFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& text, _In_ UINT32 pos) {
+ ComPtr nodeList;
+ HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"text").Get(), &nodeList);
+ if (SUCCEEDED(hr)) {
+ ComPtr node;
+ hr = nodeList->Item(pos, &node);
+ if (SUCCEEDED(hr)) {
+ hr = Util::setNodeStringValue(text, node.Get(), xml);
+ }
+ }
+ return hr;
+}
+
+
+HRESULT WinToast::setImageFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& path) {
+ assert(path.size() < MAX_PATH);
+
+ wchar_t imagePath[MAX_PATH] = L"file:///";
+ HRESULT hr = StringCchCatW(imagePath, MAX_PATH, path.c_str());
+ if (SUCCEEDED(hr)) {
+ ComPtr nodeList;
+ HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"image").Get(), &nodeList);
+ if (SUCCEEDED(hr)) {
+ ComPtr node;
+ hr = nodeList->Item(0, &node);
+ if (SUCCEEDED(hr)) {
+ ComPtr attributes;
+ hr = node->get_Attributes(&attributes);
+ if (SUCCEEDED(hr)) {
+ ComPtr editedNode;
+ hr = attributes->GetNamedItem(WinToastStringWrapper(L"src").Get(), &editedNode);
+ if (SUCCEEDED(hr)) {
+ Util::setNodeStringValue(imagePath, editedNode.Get(), xml);
+ }
+ }
+ }
+ }
+ }
+ return hr;
+}
+
+HRESULT WinToast::setAudioFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& path, _In_opt_ WinToastTemplate::AudioOption option) {
+ std::vector attrs;
+ if (!path.empty()) attrs.push_back(L"src");
+ if (option == WinToastTemplate::AudioOption::Loop) attrs.push_back(L"loop");
+ if (option == WinToastTemplate::AudioOption::Silent) attrs.push_back(L"silent");
+ Util::createElement(xml, L"toast", L"audio", attrs);
+
+ ComPtr nodeList;
+ HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"audio").Get(), &nodeList);
+ if (SUCCEEDED(hr)) {
+ ComPtr node;
+ hr = nodeList->Item(0, &node);
+ if (SUCCEEDED(hr)) {
+ ComPtr attributes;
+ hr = node->get_Attributes(&attributes);
+ if (SUCCEEDED(hr)) {
+ ComPtr editedNode;
+ if (!path.empty()) {
+ if (SUCCEEDED(hr)) {
+ hr = attributes->GetNamedItem(WinToastStringWrapper(L"src").Get(), &editedNode);
+ if (SUCCEEDED(hr)) {
+ hr = Util::setNodeStringValue(path, editedNode.Get(), xml);
+ }
+ }
+ }
+
+ if (SUCCEEDED(hr)) {
+ switch (option) {
+ case WinToastTemplate::AudioOption::Loop:
+ hr = attributes->GetNamedItem(WinToastStringWrapper(L"loop").Get(), &editedNode);
+ if (SUCCEEDED(hr)) {
+ hr = Util::setNodeStringValue(L"true", editedNode.Get(), xml);
+ }
+ break;
+ case WinToastTemplate::AudioOption::Silent:
+ hr = attributes->GetNamedItem(WinToastStringWrapper(L"silent").Get(), &editedNode);
+ if (SUCCEEDED(hr)) {
+ hr = Util::setNodeStringValue(L"true", editedNode.Get(), xml);
+ }
+ default:
+ break;
+ }
+ }
+ }
+ }
+ }
+ return hr;
+}
+
+HRESULT WinToast::addActionHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& content, _In_ const std::wstring& arguments) {
+ ComPtr nodeList;
+ HRESULT hr = xml->GetElementsByTagName(WinToastStringWrapper(L"actions").Get(), &nodeList);
+ if (SUCCEEDED(hr)) {
+ UINT32 length;
+ hr = nodeList->get_Length(&length);
+ if (SUCCEEDED(hr)) {
+ ComPtr actionsNode;
+ if (length > 0) {
+ hr = nodeList->Item(0, &actionsNode);
+ } else {
+ hr = xml->GetElementsByTagName(WinToastStringWrapper(L"toast").Get(), &nodeList);
+ if (SUCCEEDED(hr)) {
+ hr = nodeList->get_Length(&length);
+ if (SUCCEEDED(hr)) {
+ ComPtr toastNode;
+ hr = nodeList->Item(0, &toastNode);
+ if (SUCCEEDED(hr)) {
+ ComPtr toastElement;
+ hr = toastNode.As(&toastElement);
+ if (SUCCEEDED(hr))
+ hr = toastElement->SetAttribute(WinToastStringWrapper(L"template").Get(), WinToastStringWrapper(L"ToastGeneric").Get());
+ if (SUCCEEDED(hr))
+ hr = toastElement->SetAttribute(WinToastStringWrapper(L"duration").Get(), WinToastStringWrapper(L"long").Get());
+ if (SUCCEEDED(hr)) {
+ ComPtr actionsElement;
+ hr = xml->CreateElement(WinToastStringWrapper(L"actions").Get(), &actionsElement);
+ if (SUCCEEDED(hr)) {
+ hr = actionsElement.As(&actionsNode);
+ if (SUCCEEDED(hr)) {
+ ComPtr appendedChild;
+ hr = toastNode->AppendChild(actionsNode.Get(), &appendedChild);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ if (SUCCEEDED(hr)) {
+ ComPtr actionElement;
+ hr = xml->CreateElement(WinToastStringWrapper(L"action").Get(), &actionElement);
+ if (SUCCEEDED(hr))
+ hr = actionElement->SetAttribute(WinToastStringWrapper(L"content").Get(), WinToastStringWrapper(content).Get());
+ if (SUCCEEDED(hr))
+ hr = actionElement->SetAttribute(WinToastStringWrapper(L"arguments").Get(), WinToastStringWrapper(arguments).Get());
+ if (SUCCEEDED(hr)) {
+ ComPtr actionNode;
+ hr = actionElement.As(&actionNode);
+ if (SUCCEEDED(hr)) {
+ ComPtr appendedChild;
+ hr = actionsNode->AppendChild(actionNode.Get(), &appendedChild);
+ }
+ }
+ }
+ }
+ }
+ return hr;
+}
+
+void WinToast::setError(_Out_ WinToastError* error, _In_ WinToastError value) {
+ if (error) {
+ *error = value;
+ }
+}
+
+WinToastTemplate::WinToastTemplate(_In_ WinToastTemplateType type) : _type(type) {
+ static constexpr std::size_t TextFieldsCount[] = { 1, 2, 2, 3, 1, 2, 2, 3};
+ _textFields = std::vector(TextFieldsCount[type], L"");
+}
+
+WinToastTemplate::~WinToastTemplate() {
+ _textFields.clear();
+}
+
+void WinToastTemplate::setTextField(_In_ const std::wstring& txt, _In_ WinToastTemplate::TextField pos) {
+ const auto position = static_cast(pos);
+ assert(position < _textFields.size());
+ _textFields[position] = txt;
+}
+
+void WinToastTemplate::setImagePath(_In_ const std::wstring& imgPath) {
+ _imagePath = imgPath;
+}
+
+void WinToastTemplate::setAudioPath(_In_ const std::wstring& audioPath) {
+ _audioPath = audioPath;
+}
+
+void WinToastTemplate::setAudioPath(_In_ AudioSystemFile file) {
+ static const std::unordered_map Files = {
+ {AudioSystemFile::DefaultSound, L"ms-winsoundevent:Notification.Default"},
+ {AudioSystemFile::IM, L"ms-winsoundevent:Notification.IM"},
+ {AudioSystemFile::Mail, L"ms-winsoundevent:Notification.Mail"},
+ {AudioSystemFile::Reminder, L"ms-winsoundevent:Notification.Reminder"},
+ {AudioSystemFile::SMS, L"ms-winsoundevent:Notification.SMS"},
+ {AudioSystemFile::Alarm, L"ms-winsoundevent:Notification.Looping.Alarm"},
+ {AudioSystemFile::Alarm2, L"ms-winsoundevent:Notification.Looping.Alarm2"},
+ {AudioSystemFile::Alarm3, L"ms-winsoundevent:Notification.Looping.Alarm3"},
+ {AudioSystemFile::Alarm4, L"ms-winsoundevent:Notification.Looping.Alarm4"},
+ {AudioSystemFile::Alarm5, L"ms-winsoundevent:Notification.Looping.Alarm5"},
+ {AudioSystemFile::Alarm6, L"ms-winsoundevent:Notification.Looping.Alarm6"},
+ {AudioSystemFile::Alarm7, L"ms-winsoundevent:Notification.Looping.Alarm7"},
+ {AudioSystemFile::Alarm8, L"ms-winsoundevent:Notification.Looping.Alarm8"},
+ {AudioSystemFile::Alarm9, L"ms-winsoundevent:Notification.Looping.Alarm9"},
+ {AudioSystemFile::Alarm10, L"ms-winsoundevent:Notification.Looping.Alarm10"},
+ {AudioSystemFile::Call, L"ms-winsoundevent:Notification.Looping.Call"},
+ {AudioSystemFile::Call1, L"ms-winsoundevent:Notification.Looping.Call1"},
+ {AudioSystemFile::Call2, L"ms-winsoundevent:Notification.Looping.Call2"},
+ {AudioSystemFile::Call3, L"ms-winsoundevent:Notification.Looping.Call3"},
+ {AudioSystemFile::Call4, L"ms-winsoundevent:Notification.Looping.Call4"},
+ {AudioSystemFile::Call5, L"ms-winsoundevent:Notification.Looping.Call5"},
+ {AudioSystemFile::Call6, L"ms-winsoundevent:Notification.Looping.Call6"},
+ {AudioSystemFile::Call7, L"ms-winsoundevent:Notification.Looping.Call7"},
+ {AudioSystemFile::Call8, L"ms-winsoundevent:Notification.Looping.Call8"},
+ {AudioSystemFile::Call9, L"ms-winsoundevent:Notification.Looping.Call9"},
+ {AudioSystemFile::Call10, L"ms-winsoundevent:Notification.Looping.Call10"},
+ };
+ const auto iter = Files.find(file);
+ assert(iter != Files.end());
+ _audioPath = iter->second;
+}
+
+void WinToastTemplate::setAudioOption(_In_ WinToastTemplate::AudioOption audioOption) {
+ _audioOption = audioOption;
+}
+
+void WinToastTemplate::setFirstLine(const std::wstring &text) {
+ setTextField(text, WinToastTemplate::FirstLine);
+}
+
+void WinToastTemplate::setSecondLine(const std::wstring &text) {
+ setTextField(text, WinToastTemplate::SecondLine);
+}
+
+void WinToastTemplate::setThirdLine(const std::wstring &text) {
+ setTextField(text, WinToastTemplate::ThirdLine);
+}
+
+void WinToastTemplate::setDuration(_In_ Duration duration) {
+ _duration = duration;
+}
+
+void WinToastTemplate::setExpiration(_In_ INT64 millisecondsFromNow) {
+ _expiration = millisecondsFromNow;
+}
+
+void WinToastLib::WinToastTemplate::setScenario(Scenario scenario) {
+ switch (scenario) {
+ case Scenario::Default: _scenario = L"Default"; break;
+ case Scenario::Alarm: _scenario = L"Alarm"; break;
+ case Scenario::IncomingCall: _scenario = L"IncomingCall"; break;
+ case Scenario::Reminder: _scenario = L"Reminder"; break;
+ }
+}
+
+void WinToastTemplate::setAttributionText(_In_ const std::wstring& attributionText) {
+ _attributionText = attributionText;
+}
+
+void WinToastTemplate::addAction(_In_ const std::wstring & label) {
+ _actions.push_back(label);
+}
+
+std::size_t WinToastTemplate::textFieldsCount() const {
+ return _textFields.size();
+}
+
+std::size_t WinToastTemplate::actionsCount() const {
+ return _actions.size();
+}
+
+bool WinToastTemplate::hasImage() const {
+ return _type < WinToastTemplateType::Text01;
+}
+
+const std::vector& WinToastTemplate::textFields() const {
+ return _textFields;
+}
+
+const std::wstring& WinToastTemplate::textField(_In_ TextField pos) const {
+ const auto position = static_cast(pos);
+ assert(position < _textFields.size());
+ return _textFields[position];
+}
+
+const std::wstring& WinToastTemplate::actionLabel(_In_ std::size_t position) const {
+ assert(position < _actions.size());
+ return _actions[position];
+}
+
+const std::wstring& WinToastTemplate::imagePath() const {
+ return _imagePath;
+}
+
+const std::wstring& WinToastTemplate::audioPath() const {
+ return _audioPath;
+}
+
+const std::wstring& WinToastTemplate::attributionText() const {
+ return _attributionText;
+}
+
+const std::wstring& WinToastLib::WinToastTemplate::scenario() const {
+ return _scenario;
+}
+
+INT64 WinToastTemplate::expiration() const {
+ return _expiration;
+}
+
+WinToastTemplate::WinToastTemplateType WinToastTemplate::type() const {
+ return _type;
+}
+
+WinToastTemplate::AudioOption WinToastTemplate::audioOption() const {
+ return _audioOption;
+}
+
+WinToastTemplate::Duration WinToastTemplate::duration() const {
+ return _duration;
+}
diff --git a/third_party/WinToast/wintoastlib.h b/third_party/WinToast/wintoastlib.h
new file mode 100644
index 000000000000..93546dc5b8fe
--- /dev/null
+++ b/third_party/WinToast/wintoastlib.h
@@ -0,0 +1,234 @@
+/* * Copyright (C) 2016-2019 Mohammed Boujemaoui
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to
+ * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ * the Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+ * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+
+#ifndef WINTOASTLIB_H
+#define WINTOASTLIB_H
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
diff --git a/toolkit/mozapps/defaultagent/BackgroundTask_defaultagent.sys.mjs b/toolkit/mozapps/defaultagent/BackgroundTask_defaultagent.sys.mjs
index 691f76c3199b..e6eca7b0bec0 100644
--- a/toolkit/mozapps/defaultagent/BackgroundTask_defaultagent.sys.mjs
+++ b/toolkit/mozapps/defaultagent/BackgroundTask_defaultagent.sys.mjs
@@ -17,6 +17,8 @@ const EXIT_CODE = {
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
setTimeout: "resource://gre/modules/Timer.sys.mjs",
+ BackgroundTasksUtils: "resource://gre/modules/BackgroundTasksUtils.sys.mjs",
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
// eslint-disable-next-line mozilla/no-browser-refs-in-toolkit
ShellService: "resource:///modules/ShellService.sys.mjs",
});
@@ -155,11 +157,25 @@ export async function runBackgroundTask(commandLine) {
lazy.log.info(`Running do-task with AUMID "${aumid}"`);
+ let cppFallback = false;
try {
- lazy.log.info("Running JS do-task.");
- await runWithRegistryLocked(async () => {
- await doTask(defaultAgent, force);
- });
+ await lazy.BackgroundTasksUtils.enableNimbus(commandLine);
+ cppFallback =
+ lazy.NimbusFeatures.defaultAgent.getVariable("cppFallback");
+ } catch (e) {
+ lazy.log.error(`Error enabling nimbus: ${e}`);
+ }
+
+ try {
+ if (!cppFallback) {
+ lazy.log.info("Running JS do-task.");
+ await runWithRegistryLocked(async () => {
+ await doTask(defaultAgent, force);
+ });
+ } else {
+ lazy.log.info("Running C++ do-task.");
+ defaultAgent.doTask(aumid, force);
+ }
} catch (e) {
if (e.message) {
lazy.log.error(e.message);
diff --git a/toolkit/mozapps/defaultagent/DefaultAgent.cpp b/toolkit/mozapps/defaultagent/DefaultAgent.cpp
index 3bff6e3243a7..2ebb5e466ef9 100644
--- a/toolkit/mozapps/defaultagent/DefaultAgent.cpp
+++ b/toolkit/mozapps/defaultagent/DefaultAgent.cpp
@@ -7,8 +7,11 @@
#include
#include
#include
+#include
+#include
#include "nsAutoRef.h"
+#include "nsDebug.h"
#include "nsProxyRelease.h"
#include "nsWindowsHelpers.h"
#include "nsString.h"
@@ -301,6 +304,62 @@ DefaultAgent::Uninstall(const nsAString& aUniqueToken) {
}
NS_IMETHODIMP
+DefaultAgent::DoTask(const nsAString& aUniqueToken, const bool aForce) {
+ // Acquire() has a short timeout. Since this runs in the background, we
+ // could use a longer timeout in this situation. However, if another
+ // installation's agent is already running, it will update CurrentDefault,
+ // possibly send a ping, and possibly show a notification.
+ // Once all that has happened, there is no real reason to do it again. We
+ // only send one ping per day, so we aren't going to do that again. And
+ // the only time we ever show a second notification is 7 days after the
+ // first one, so we aren't going to do that again either.
+ // If the other process didn't take those actions, there is no reason that
+ // this process would take them.
+ // If the other process fails, this one will most likely fail for the same
+ // reason.
+ // So we'll just bail if we can't get the mutex quickly.
+ RegistryMutex regMutex;
+ if (!regMutex.Acquire()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Check that Firefox ran recently, if not then stop here.
+ // Also stop if no timestamp was found, which most likely indicates
+ // that Firefox was not yet run.
+ bool ranRecently = false;
+ if (!aForce && (!CheckIfAppRanRecently(&ranRecently) || !ranRecently)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ DefaultBrowserResult defaultBrowserResult = GetDefaultBrowserInfo();
+ DefaultBrowserInfo browserInfo{};
+ if (defaultBrowserResult.isOk()) {
+ browserInfo = defaultBrowserResult.unwrap();
+ } else {
+ browserInfo.currentDefaultBrowser = Browser::Error;
+ browserInfo.previousDefaultBrowser = Browser::Error;
+ }
+
+ DefaultPdfResult defaultPdfResult = GetDefaultPdfInfo();
+ DefaultPdfInfo pdfInfo{};
+ if (defaultPdfResult.isOk()) {
+ pdfInfo = defaultPdfResult.unwrap();
+ } else {
+ pdfInfo.currentDefaultPdf = PDFHandler::Error;
+ }
+
+ NotificationActivities activitiesPerformed;
+ // We block while waiting for the notification which prevents STA thread
+ // callbacks from running as the event loop won't run. Moving notification
+ // handling to an MTA thread prevents this conflict.
+ activitiesPerformed = MaybeShowNotification(
+ browserInfo, PromiseFlatString(aUniqueToken).get(), aForce);
+
+ HRESULT hr = SendDefaultAgentPing(browserInfo, pdfInfo, activitiesPerformed);
+ return SUCCEEDED(hr) ? NS_OK : NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
DefaultAgent::AppRanRecently(bool* aRanRecently) {
bool ranRecently = false;
*aRanRecently = CheckIfAppRanRecently(&ranRecently) && ranRecently;
diff --git a/toolkit/mozapps/defaultagent/DefaultBrowser.cpp b/toolkit/mozapps/defaultagent/DefaultBrowser.cpp
index d9543665b799..87d3f62632e5 100644
--- a/toolkit/mozapps/defaultagent/DefaultBrowser.cpp
+++ b/toolkit/mozapps/defaultagent/DefaultBrowser.cpp
@@ -184,6 +184,17 @@ BrowserResult TryGetReplacePreviousDefaultBrowser(Browser currentDefault) {
return GetBrowserFromString(previousDefault);
}
+DefaultBrowserResult GetDefaultBrowserInfo() {
+ DefaultBrowserInfo browserInfo;
+
+ MOZ_TRY_VAR(browserInfo.currentDefaultBrowser, TryGetDefaultBrowser());
+ MOZ_TRY_VAR(
+ browserInfo.previousDefaultBrowser,
+ TryGetReplacePreviousDefaultBrowser(browserInfo.currentDefaultBrowser));
+
+ return browserInfo;
+}
+
// We used to prefix this key with the installation directory, but that causes
// problems with our new "only one ping per day across installs" restriction.
// To make sure all installations use consistent data, the value's name is
diff --git a/toolkit/mozapps/defaultagent/DefaultBrowser.h b/toolkit/mozapps/defaultagent/DefaultBrowser.h
index 081f3b9ccfb6..f1b940959fbc 100644
--- a/toolkit/mozapps/defaultagent/DefaultBrowser.h
+++ b/toolkit/mozapps/defaultagent/DefaultBrowser.h
@@ -10,6 +10,7 @@
#include
#include "mozilla/DefineEnum.h"
+#include "mozilla/WinHeaderOnlyUtils.h"
namespace mozilla::default_agent {
@@ -23,6 +24,9 @@ struct DefaultBrowserInfo {
Browser previousDefaultBrowser;
};
+using DefaultBrowserResult = mozilla::WindowsErrorResult;
+
+DefaultBrowserResult GetDefaultBrowserInfo();
Browser GetDefaultBrowser();
Browser GetReplacePreviousDefaultBrowser(Browser currentBrowser);
diff --git a/toolkit/mozapps/defaultagent/Notification.cpp b/toolkit/mozapps/defaultagent/Notification.cpp
index 39236eac1562..844ef0486092 100644
--- a/toolkit/mozapps/defaultagent/Notification.cpp
+++ b/toolkit/mozapps/defaultagent/Notification.cpp
@@ -7,10 +7,36 @@
#include "Notification.h"
#include
+#include
#include
#include
-#include "nsLiteralString.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/CmdLineAndEnvUtils.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/mscom/EnsureMTA.h"
+#include "mozilla/intl/FileSource.h"
+#include "mozilla/intl/Localization.h"
+#include "mozilla/ShellHeaderOnlyUtils.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Unused.h"
+#include "mozilla/WinHeaderOnlyUtils.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsWindowsHelpers.h"
+#include "readstrings.h"
+#include "updatererrors.h"
+#include "WindowsDefaultBrowser.h"
+
+#include "common.h"
+#include "DefaultBrowser.h"
+#include "EventLog.h"
+#include "Registry.h"
+#include "SetDefaultBrowser.h"
+
+#include "wintoastlib.h"
+
+using mozilla::intl::Localization;
#define SEVEN_DAYS_IN_SECONDS (7 * 24 * 60 * 60)
@@ -23,6 +49,574 @@
namespace mozilla::default_agent {
+bool FirefoxInstallIsEnglish();
+
+static bool SetInitialNotificationShown(bool wasShown) {
+ return !RegistrySetValueBool(IsPrefixed::Unprefixed,
+ L"InitialNotificationShown", wasShown)
+ .isErr();
+}
+
+static bool GetInitialNotificationShown() {
+ return RegistryGetValueBool(IsPrefixed::Unprefixed,
+ L"InitialNotificationShown")
+ .unwrapOr(mozilla::Some(false))
+ .valueOr(false);
+}
+
+static bool ResetInitialNotificationShown() {
+ return RegistryDeleteValue(IsPrefixed::Unprefixed,
+ L"InitialNotificationShown")
+ .isOk();
+}
+
+static bool SetFollowupNotificationShown(bool wasShown) {
+ return !RegistrySetValueBool(IsPrefixed::Unprefixed,
+ L"FollowupNotificationShown", wasShown)
+ .isErr();
+}
+
+static bool GetFollowupNotificationShown() {
+ return RegistryGetValueBool(IsPrefixed::Unprefixed,
+ L"FollowupNotificationShown")
+ .unwrapOr(mozilla::Some(false))
+ .valueOr(false);
+}
+
+static bool SetFollowupNotificationSuppressed(bool value) {
+ return !RegistrySetValueBool(IsPrefixed::Unprefixed,
+ L"FollowupNotificationSuppressed", value)
+ .isErr();
+}
+
+static bool GetFollowupNotificationSuppressed() {
+ return RegistryGetValueBool(IsPrefixed::Unprefixed,
+ L"FollowupNotificationSuppressed")
+ .unwrapOr(mozilla::Some(false))
+ .valueOr(false);
+}
+
+// Returns 0 if no value is set.
+static ULONGLONG GetFollowupNotificationRequestTime() {
+ return RegistryGetValueQword(IsPrefixed::Unprefixed, L"FollowupRequestTime")
+ .unwrapOr(mozilla::Some(0))
+ .valueOr(0);
+}
+
+// Returns false if no value is set.
+static bool GetPrefSetDefaultBrowserUserChoice() {
+ return RegistryGetValueBool(IsPrefixed::Prefixed,
+ L"SetDefaultBrowserUserChoice")
+ .unwrapOr(mozilla::Some(false))
+ .valueOr(false);
+}
+
+struct ToastStrings {
+ mozilla::UniquePtr text1;
+ mozilla::UniquePtr text2;
+ mozilla::UniquePtr action1;
+ mozilla::UniquePtr action2;
+ mozilla::UniquePtr relImagePath;
+};
+
+struct Strings {
+ // Toast notification button text is hard to localize because it tends to
+ // overflow. Thus, we have 3 different toast notifications:
+ // - The initial notification, which includes a button with text like
+ // "Ask me later". Since we cannot easily localize this, we will display
+ // it only in English.
+ // - The followup notification, to be shown if the user clicked "Ask me
+ // later". Since we only have that button in English, we only need this
+ // notification in English.
+ // - The localized notification, which has much shorter button text to
+ // (hopefully) prevent overflow: just "Yes" and "No". Since we no longer
+ // have an "Ask me later" button, a followup localized notification is not
+ // needed.
+ ToastStrings initialToast;
+ ToastStrings followupToast;
+ ToastStrings localizedToast;
+
+ // Returned pointer points within this struct and should not be freed.
+ const ToastStrings* GetToastStrings(NotificationType whichToast,
+ bool englishStrings) const {
+ if (!englishStrings) {
+ return &localizedToast;
+ }
+ if (whichToast == NotificationType::Initial) {
+ return &initialToast;
+ }
+ return &followupToast;
+ }
+};
+
+// Gets all strings out of the relevant INI files.
+// Returns true on success, false on failure
+static bool GetStrings(Strings& strings) {
+ mozilla::UniquePtr installPath;
+ bool success = GetInstallDirectory(installPath);
+ if (!success) {
+ LOG_ERROR_MESSAGE(L"Failed to get install directory when getting strings");
+ return false;
+ }
+ nsTArray resIds = {"branding/brand.ftl"_ns,
+ "browser/backgroundtasks/defaultagent.ftl"_ns};
+ RefPtr l10n = Localization::Create(resIds, true);
+ nsAutoCString daHeaderText, daBodyText, daYesButton, daNoButton;
+ mozilla::ErrorResult daRv;
+ l10n->FormatValueSync("default-browser-notification-header-text"_ns, {},
+ daHeaderText, daRv);
+ ENSURE_SUCCESS(daRv, false);
+ l10n->FormatValueSync("default-browser-notification-body-text"_ns, {},
+ daBodyText, daRv);
+ ENSURE_SUCCESS(daRv, false);
+ l10n->FormatValueSync("default-browser-notification-yes-button-text"_ns, {},
+ daYesButton, daRv);
+ ENSURE_SUCCESS(daRv, false);
+ l10n->FormatValueSync("default-browser-notification-no-button-text"_ns, {},
+ daNoButton, daRv);
+ ENSURE_SUCCESS(daRv, false);
+
+ NS_ConvertUTF8toUTF16 daHeaderTextW(daHeaderText), daBodyTextW(daBodyText),
+ daYesButtonW(daYesButton), daNoButtonW(daNoButton);
+ strings.localizedToast.text1 =
+ mozilla::MakeUnique(daHeaderTextW.Length() + 1);
+ wcsncpy(strings.localizedToast.text1.get(), daHeaderTextW.get(),
+ daHeaderTextW.Length() + 1);
+ strings.localizedToast.text2 =
+ mozilla::MakeUnique(daBodyTextW.Length() + 1);
+ wcsncpy(strings.localizedToast.text2.get(), daBodyTextW.get(),
+ daBodyTextW.Length() + 1);
+ strings.localizedToast.action1 =
+ mozilla::MakeUnique(daYesButtonW.Length() + 1);
+ wcsncpy(strings.localizedToast.action1.get(), daYesButtonW.get(),
+ daYesButtonW.Length() + 1);
+ strings.localizedToast.action2 =
+ mozilla::MakeUnique(daNoButtonW.Length() + 1);
+ wcsncpy(strings.localizedToast.action2.get(), daNoButtonW.get(),
+ daNoButtonW.Length() + 1);
+ const wchar_t* iniFormat = L"%s\\defaultagent.ini";
+ int bufferSize = _scwprintf(iniFormat, installPath.get());
+ ++bufferSize; // Extra character for terminating null
+ mozilla::UniquePtr iniPath =
+ mozilla::MakeUnique(bufferSize);
+ _snwprintf_s(iniPath.get(), bufferSize, _TRUNCATE, iniFormat,
+ installPath.get());
+
+ IniReader nonlocalizedReader(iniPath.get(), "Nonlocalized");
+ nonlocalizedReader.AddKey("InitialToastRelativeImagePath",
+ &strings.initialToast.relImagePath);
+ nonlocalizedReader.AddKey("FollowupToastRelativeImagePath",
+ &strings.followupToast.relImagePath);
+ nonlocalizedReader.AddKey("LocalizedToastRelativeImagePath",
+ &strings.localizedToast.relImagePath);
+ int result = nonlocalizedReader.Read();
+ if (result != OK) {
+ LOG_ERROR_MESSAGE(L"Unable to read non-localized strings: %d", result);
+ return false;
+ }
+
+ return true;
+}
+
+static mozilla::WindowsError LaunchFirefoxToHandleDefaultBrowserAgent() {
+ // Could also be `MOZ_APP_NAME.exe`, but there's no generality to be gained:
+ // the WDBA is Firefox-only.
+ FilePathResult firefoxPathResult = GetRelativeBinaryPath(L"firefox.exe");
+ if (firefoxPathResult.isErr()) {
+ return firefoxPathResult.unwrapErr();
+ }
+ std::wstring firefoxPath = firefoxPathResult.unwrap();
+
+ _bstr_t cmd = firefoxPath.c_str();
+ // Omit argv[0] because ShellExecute doesn't need it.
+ _variant_t args(L"-to-handle-default-browser-agent");
+ _variant_t operation(L"open");
+ _variant_t directory;
+ _variant_t showCmd(SW_SHOWNORMAL);
+
+ // To prevent inheriting environment variables from the background task, we
+ // run Firefox via Explorer instead of our own process. This mimics the
+ // implementation of the Windows Launcher Process.
+ auto result =
+ ShellExecuteByExplorer(cmd, args, operation, directory, showCmd);
+ NS_ENSURE_TRUE(result.isOk(), result.unwrapErr());
+
+ return mozilla::WindowsError::CreateSuccess();
+}
+
+/*
+ * Set the default browser.
+ *
+ * First check if we can directly write UserChoice, if so attempt that.
+ * If we can't write UserChoice, or if the attempt fails, fall back to
+ * showing the Default Apps page of Settings.
+ *
+ * @param aAumi The AUMI of the installation to set as default.
+ */
+static void SetDefaultBrowserFromNotification(const wchar_t* aumi) {
+ nsresult rv = NS_ERROR_FAILURE;
+ if (GetPrefSetDefaultBrowserUserChoice()) {
+ rv = SetDefaultBrowserUserChoice(aumi);
+ }
+
+ if (NS_SUCCEEDED(rv)) {
+ mozilla::Unused << LaunchFirefoxToHandleDefaultBrowserAgent();
+ } else {
+ LOG_ERROR_MESSAGE(L"Failed to SetDefaultBrowserUserChoice: %#X",
+ GetLastError());
+ LaunchModernSettingsDialogDefaultApps();
+ }
+}
+
+// This encapsulates the data that needs to be protected by a mutex because it
+// will be shared by the main thread and the handler thread.
+// To ensure the data is only written once, handlerDataHasBeenSet should be
+// initialized to false, then set to true when the handler writes data into the
+// structure.
+struct HandlerData {
+ NotificationActivities activitiesPerformed;
+ bool handlerDataHasBeenSet;
+};
+
+// The value that ToastHandler writes into should be a global. We can't control
+// when ToastHandler is called, and if this value isn't a global, ToastHandler
+// may be called and attempt to access this after it has been deconstructed.
+// Since this value is accessed by the handler thread and the main thread, it
+// is protected by a mutex (gHandlerMutex).
+// Since ShowNotification deconstructs the mutex, it might seem like once
+// ShowNotification exits, we can just rely on the inability to wait on an
+// invalid mutex to protect the deconstructed data, but it's possible that
+// we could deconstruct the mutex while the handler is holding it and is
+// already accessing the protected data.
+static HandlerData gHandlerReturnData;
+static HANDLE gHandlerMutex = INVALID_HANDLE_VALUE;
+
+class ToastHandler : public WinToastLib::IWinToastHandler {
+ private:
+ NotificationType mWhichNotification;
+ HANDLE mEvent;
+ const std::wstring mAumiStr;
+
+ public:
+ ToastHandler(NotificationType whichNotification, HANDLE event,
+ const wchar_t* aumi)
+ : mWhichNotification(whichNotification), mEvent(event), mAumiStr(aumi) {}
+
+ void FinishHandler(NotificationActivities& returnData) const {
+ SetReturnData(returnData);
+
+ BOOL success = SetEvent(mEvent);
+ if (!success) {
+ LOG_ERROR_MESSAGE(L"Event could not be set: %#X", GetLastError());
+ }
+ }
+
+ void SetReturnData(NotificationActivities& toSet) const {
+ DWORD result = WaitForSingleObject(gHandlerMutex, MUTEX_TIMEOUT_MS);
+ if (result == WAIT_TIMEOUT) {
+ LOG_ERROR_MESSAGE(L"Unable to obtain mutex ownership");
+ return;
+ } else if (result == WAIT_FAILED) {
+ LOG_ERROR_MESSAGE(L"Failed to wait on mutex: %#X", GetLastError());
+ return;
+ } else if (result == WAIT_ABANDONED) {
+ LOG_ERROR_MESSAGE(L"Found abandoned mutex");
+ ReleaseMutex(gHandlerMutex);
+ return;
+ }
+
+ // Only set this data once
+ if (!gHandlerReturnData.handlerDataHasBeenSet) {
+ gHandlerReturnData.activitiesPerformed = toSet;
+ gHandlerReturnData.handlerDataHasBeenSet = true;
+ }
+
+ BOOL success = ReleaseMutex(gHandlerMutex);
+ if (!success) {
+ LOG_ERROR_MESSAGE(L"Unable to release mutex ownership: %#X",
+ GetLastError());
+ }
+ }
+
+ void toastActivated() const override {
+ NotificationActivities activitiesPerformed;
+ activitiesPerformed.type = mWhichNotification;
+ activitiesPerformed.shown = NotificationShown::Shown;
+ activitiesPerformed.action = NotificationAction::ToastClicked;
+
+ // Notification strings are written to indicate the default browser is
+ // restored to Firefox when the notification body is clicked to prevent
+ // ambiguity when buttons aren't pressed.
+ SetDefaultBrowserFromNotification(mAumiStr.c_str());
+
+ FinishHandler(activitiesPerformed);
+ }
+
+ void toastActivated(int actionIndex) const override {
+ NotificationActivities activitiesPerformed;
+ activitiesPerformed.type = mWhichNotification;
+ activitiesPerformed.shown = NotificationShown::Shown;
+ // Override this below
+ activitiesPerformed.action = NotificationAction::NoAction;
+
+ if (actionIndex == 0) {
+ // "Make Firefox the default" button, on both the initial and followup
+ // notifications. "Yes" button on the localized notification.
+ activitiesPerformed.action = NotificationAction::MakeFirefoxDefaultButton;
+
+ SetDefaultBrowserFromNotification(mAumiStr.c_str());
+ } else if (actionIndex == 1) {
+ // Do nothing. As long as we don't call
+ // SetFollowupNotificationRequestTime, there will be no followup
+ // notification.
+ activitiesPerformed.action = NotificationAction::DismissedByButton;
+ }
+
+ FinishHandler(activitiesPerformed);
+ }
+
+ void toastDismissed(WinToastDismissalReason state) const override {
+ NotificationActivities activitiesPerformed;
+ activitiesPerformed.type = mWhichNotification;
+ activitiesPerformed.shown = NotificationShown::Shown;
+ // Override this below
+ activitiesPerformed.action = NotificationAction::NoAction;
+
+ if (state == WinToastDismissalReason::TimedOut) {
+ activitiesPerformed.action = NotificationAction::DismissedByTimeout;
+ } else if (state == WinToastDismissalReason::ApplicationHidden) {
+ activitiesPerformed.action =
+ NotificationAction::DismissedByApplicationHidden;
+ } else if (state == WinToastDismissalReason::UserCanceled) {
+ activitiesPerformed.action = NotificationAction::DismissedToActionCenter;
+ }
+
+ FinishHandler(activitiesPerformed);
+ }
+
+ void toastFailed() const override {
+ NotificationActivities activitiesPerformed;
+ activitiesPerformed.type = mWhichNotification;
+ activitiesPerformed.shown = NotificationShown::Error;
+ activitiesPerformed.action = NotificationAction::NoAction;
+
+ LOG_ERROR_MESSAGE(L"Toast notification failed to display");
+ FinishHandler(activitiesPerformed);
+ }
+};
+
+// This function blocks until the shown notification is activated or dismissed.
+static NotificationActivities ShowNotification(
+ NotificationType whichNotification, const wchar_t* aumi) {
+ // Initially set the value that will be returned to error. If the notification
+ // is shown successfully, we'll update it.
+ NotificationActivities activitiesPerformed = {whichNotification,
+ NotificationShown::Error,
+ NotificationAction::NoAction};
+
+ bool isEnglishInstall = FirefoxInstallIsEnglish();
+
+ Strings strings;
+ if (!GetStrings(strings)) {
+ return activitiesPerformed;
+ }
+ const ToastStrings* toastStrings =
+ strings.GetToastStrings(whichNotification, isEnglishInstall);
+
+ mozilla::mscom::EnsureMTA([&] {
+ using namespace WinToastLib;
+
+ if (!WinToast::isCompatible()) {
+ LOG_ERROR_MESSAGE(L"System is not compatible with WinToast");
+ return;
+ }
+ WinToast::instance()->setAppName(L"" MOZ_APP_DISPLAYNAME);
+ std::wstring aumiStr = aumi;
+ WinToast::instance()->setAppUserModelId(aumiStr);
+ WinToast::instance()->setShortcutPolicy(
+ WinToastLib::WinToast::SHORTCUT_POLICY_REQUIRE_NO_CREATE);
+ WinToast::WinToastError error;
+ if (!WinToast::instance()->initialize(&error)) {
+ LOG_ERROR_MESSAGE(WinToast::strerror(error).c_str());
+ return;
+ }
+
+ // This event object will let the handler notify us when it has handled the
+ // notification.
+ nsAutoHandle event(CreateEventW(nullptr, TRUE, FALSE, nullptr));
+ if (event.get() == nullptr) {
+ LOG_ERROR_MESSAGE(L"Unable to create event object: %#X", GetLastError());
+ return;
+ }
+
+ bool success = false;
+ if (whichNotification == NotificationType::Initial) {
+ success = SetInitialNotificationShown(true);
+ } else {
+ success = SetFollowupNotificationShown(true);
+ }
+ if (!success) {
+ // Return early in this case to prevent the notification from being shown
+ // on every run.
+ LOG_ERROR_MESSAGE(L"Unable to set notification as displayed");
+ return;
+ }
+
+ // We need the absolute image path, not the relative path.
+ mozilla::UniquePtr installPath;
+ success = GetInstallDirectory(installPath);
+ if (!success) {
+ LOG_ERROR_MESSAGE(L"Failed to get install directory for the image path");
+ return;
+ }
+ const wchar_t* absPathFormat = L"%s\\%s";
+ int bufferSize = _scwprintf(absPathFormat, installPath.get(),
+ toastStrings->relImagePath.get());
+ ++bufferSize; // Extra character for terminating null
+ mozilla::UniquePtr absImagePath =
+ mozilla::MakeUnique(bufferSize);
+ _snwprintf_s(absImagePath.get(), bufferSize, _TRUNCATE, absPathFormat,
+ installPath.get(), toastStrings->relImagePath.get());
+
+ // This is used to protect gHandlerReturnData.
+ gHandlerMutex = CreateMutexW(nullptr, TRUE, nullptr);
+ if (gHandlerMutex == nullptr) {
+ LOG_ERROR_MESSAGE(L"Unable to create mutex: %#X", GetLastError());
+ return;
+ }
+ // Automatically close this mutex when this function exits.
+ nsAutoHandle autoMutex(gHandlerMutex);
+ // No need to initialize gHandlerReturnData.activitiesPerformed, since it
+ // will be set by the handler. But we do need to initialize
+ // gHandlerReturnData.handlerDataHasBeenSet so the handler knows that no
+ // data has been set yet.
+ gHandlerReturnData.handlerDataHasBeenSet = false;
+ success = ReleaseMutex(gHandlerMutex);
+ if (!success) {
+ LOG_ERROR_MESSAGE(L"Unable to release mutex ownership: %#X",
+ GetLastError());
+ }
+
+ // Finally ready to assemble the notification and dispatch it.
+ WinToastTemplate toastTemplate =
+ WinToastTemplate(WinToastTemplate::ImageAndText02);
+ toastTemplate.setTextField(toastStrings->text1.get(),
+ WinToastTemplate::FirstLine);
+ toastTemplate.setTextField(toastStrings->text2.get(),
+ WinToastTemplate::SecondLine);
+ toastTemplate.addAction(toastStrings->action1.get());
+ toastTemplate.addAction(toastStrings->action2.get());
+ toastTemplate.setImagePath(absImagePath.get());
+ toastTemplate.setScenario(WinToastTemplate::Scenario::Reminder);
+ ToastHandler* handler =
+ new ToastHandler(whichNotification, event.get(), aumi);
+ INT64 id = WinToast::instance()->showToast(toastTemplate, handler, &error);
+ if (id < 0) {
+ LOG_ERROR_MESSAGE(WinToast::strerror(error).c_str());
+ return;
+ }
+
+ DWORD result =
+ WaitForSingleObject(event.get(), NOTIFICATION_WAIT_TIMEOUT_MS);
+ // Don't return after these errors. Attempt to hide the notification.
+ if (result == WAIT_FAILED) {
+ LOG_ERROR_MESSAGE(L"Unable to wait on event object: %#X", GetLastError());
+ } else if (result == WAIT_TIMEOUT) {
+ LOG_ERROR_MESSAGE(L"Timed out waiting for event object");
+ } else {
+ result = WaitForSingleObject(gHandlerMutex, MUTEX_TIMEOUT_MS);
+ if (result == WAIT_TIMEOUT) {
+ LOG_ERROR_MESSAGE(L"Unable to obtain mutex ownership");
+ // activitiesPerformed is already set to error. No change needed.
+ } else if (result == WAIT_FAILED) {
+ LOG_ERROR_MESSAGE(L"Failed to wait on mutex: %#X", GetLastError());
+ // activitiesPerformed is already set to error. No change needed.
+ } else if (result == WAIT_ABANDONED) {
+ LOG_ERROR_MESSAGE(L"Found abandoned mutex");
+ ReleaseMutex(gHandlerMutex);
+ // activitiesPerformed is already set to error. No change needed.
+ } else {
+ // Mutex is being held. It is safe to access gHandlerReturnData.
+ // If gHandlerReturnData.handlerDataHasBeenSet is false, the handler
+ // never ran. Use the error value activitiesPerformed already contains.
+ if (gHandlerReturnData.handlerDataHasBeenSet) {
+ activitiesPerformed = gHandlerReturnData.activitiesPerformed;
+ }
+
+ success = ReleaseMutex(gHandlerMutex);
+ if (!success) {
+ LOG_ERROR_MESSAGE(L"Unable to release mutex ownership: %#X",
+ GetLastError());
+ }
+ }
+ }
+
+ if (!WinToast::instance()->hideToast(id)) {
+ LOG_ERROR_MESSAGE(L"Failed to hide notification");
+ }
+ });
+ return activitiesPerformed;
+}
+
+// Previously this function checked that the Firefox build was using English.
+// This was checked because of the peculiar way we were localizing toast
+// notifications where we used a completely different set of strings in English.
+//
+// We've since unified the notification flows but need to clean up unused code
+// and config files - Bug 1826375.
+bool FirefoxInstallIsEnglish() { return false; }
+
+// If a notification is shown, this function will block until the notification
+// is activated or dismissed.
+// aumi is the App User Model ID.
+NotificationActivities MaybeShowNotification(
+ const DefaultBrowserInfo& browserInfo, const wchar_t* aumi, bool force) {
+ // Default to not showing a notification. Any other value will be returned
+ // directly from ShowNotification.
+ NotificationActivities activitiesPerformed = {NotificationType::Initial,
+ NotificationShown::NotShown,
+ NotificationAction::NoAction};
+
+ // Reset notification state machine, user setting default browser to Firefox
+ // is a strong signal that they intend to have it as the default browser.
+ if (browserInfo.currentDefaultBrowser == Browser::Firefox) {
+ ResetInitialNotificationShown();
+ }
+
+ bool initialNotificationShown = GetInitialNotificationShown();
+ if (!initialNotificationShown || force) {
+ if ((browserInfo.currentDefaultBrowser == Browser::EdgeWithBlink &&
+ browserInfo.previousDefaultBrowser == Browser::Firefox) ||
+ force) {
+ return ShowNotification(NotificationType::Initial, aumi);
+ }
+ return activitiesPerformed;
+ }
+ activitiesPerformed.type = NotificationType::Followup;
+
+ ULONGLONG followupNotificationRequestTime =
+ GetFollowupNotificationRequestTime();
+ bool followupNotificationRequested = followupNotificationRequestTime != 0;
+ bool followupNotificationShown = GetFollowupNotificationShown();
+ if (followupNotificationRequested && !followupNotificationShown &&
+ !GetFollowupNotificationSuppressed()) {
+ ULONGLONG secondsSinceRequestTime =
+ SecondsPassedSince(followupNotificationRequestTime);
+
+ if (secondsSinceRequestTime >= SEVEN_DAYS_IN_SECONDS) {
+ // If we go to show the followup notification and the user has already
+ // changed the default browser, permanently suppress the followup since
+ // it's no longer relevant.
+ if (browserInfo.currentDefaultBrowser == Browser::EdgeWithBlink) {
+ return ShowNotification(NotificationType::Followup, aumi);
+ } else {
+ SetFollowupNotificationSuppressed(true);
+ }
+ }
+ }
+ return activitiesPerformed;
+}
+
std::string GetStringForNotificationType(NotificationType type) {
switch (type) {
case NotificationType::Initial:
diff --git a/toolkit/mozapps/defaultagent/Notification.h b/toolkit/mozapps/defaultagent/Notification.h
index e4e007088da4..210c55f55947 100644
--- a/toolkit/mozapps/defaultagent/Notification.h
+++ b/toolkit/mozapps/defaultagent/Notification.h
@@ -7,9 +7,7 @@
#ifndef __DEFAULT_BROWSER_NOTIFICATION_H__
#define __DEFAULT_BROWSER_NOTIFICATION_H__
-#include
-
-#include "nsStringFwd.h"
+#include "DefaultBrowser.h"
namespace mozilla::default_agent {
@@ -41,6 +39,9 @@ struct NotificationActivities {
NotificationAction action;
};
+NotificationActivities MaybeShowNotification(
+ const DefaultBrowserInfo& browserInfo, const wchar_t* aumi, bool force);
+
// These take enum values and get strings suitable for telemetry
std::string GetStringForNotificationType(NotificationType type);
std::string GetStringForNotificationShown(NotificationShown shown);
diff --git a/toolkit/mozapps/defaultagent/defaultagent.ini b/toolkit/mozapps/defaultagent/defaultagent.ini
new file mode 100644
index 000000000000..9300b20c46b6
--- /dev/null
+++ b/toolkit/mozapps/defaultagent/defaultagent.ini
@@ -0,0 +1,9 @@
+; 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/.
+
+; This file is in the UTF-8 encoding
+[Nonlocalized]
+InitialToastRelativeImagePath=browser/VisualElements/VisualElements_150.png
+FollowupToastRelativeImagePath=browser/VisualElements/VisualElements_150.png
+LocalizedToastRelativeImagePath=browser/VisualElements/VisualElements_150.png
diff --git a/toolkit/mozapps/defaultagent/moz.build b/toolkit/mozapps/defaultagent/moz.build
index 2e2ed1c40fa2..86b68c6371e7 100644
--- a/toolkit/mozapps/defaultagent/moz.build
+++ b/toolkit/mozapps/defaultagent/moz.build
@@ -26,10 +26,20 @@ UNIFIED_SOURCES += [
]
SOURCES += [
+ "/third_party/WinToast/wintoastlib.cpp",
"/toolkit/mozapps/update/common/readstrings.cpp",
"Notification.cpp",
]
+# Suppress warnings from third-party code.
+SOURCES["/third_party/WinToast/wintoastlib.cpp"].flags += [
+ "-Wno-implicit-fallthrough",
+ "-Wno-nonportable-include-path", # Needed for wintoastlib.h including "Windows.h"
+]
+SOURCES["Notification.cpp"].flags += [
+ "-Wno-nonportable-include-path", # Needed for wintoastlib.h including "Windows.h"
+]
+
EXPORTS.mozilla += [
"DefaultAgent.h",
"WindowsMutex.h",
@@ -42,6 +52,7 @@ USE_LIBS += [
LOCAL_INCLUDES += [
"/browser/components/shell/",
"/other-licenses/nsis/Contrib/CityHash/cityhash",
+ "/third_party/WinToast",
"/toolkit/components/jsoncpp/include",
"/toolkit/mozapps/update/common",
]
@@ -87,6 +98,8 @@ for var in ("MOZ_APP_BASENAME", "MOZ_APP_DISPLAYNAME", "MOZ_APP_VENDOR"):
DEFINES["UNICODE"] = True
DEFINES["_UNICODE"] = True
+FINAL_TARGET_FILES += ["defaultagent.ini"]
+
FINAL_LIBRARY = "xul"
if CONFIG["ENABLE_TESTS"]:
diff --git a/toolkit/mozapps/defaultagent/nsIDefaultAgent.idl b/toolkit/mozapps/defaultagent/nsIDefaultAgent.idl
index 8472dea3afc1..7e78e1b30d38 100644
--- a/toolkit/mozapps/defaultagent/nsIDefaultAgent.idl
+++ b/toolkit/mozapps/defaultagent/nsIDefaultAgent.idl
@@ -52,6 +52,20 @@ interface nsIDefaultAgent : nsISupports
void uninstall(in AString aUniqueToken);
/**
+ * Actually performs the default agent task, which currently means generating
+ * and sending our telemetry ping and possibly showing a notification to the
+ * user if their browser has switched from Firefox to Edge with Blink.
+ *
+ * @param {AString} aUniqueToken
+ * A unique identifier for this installation; the same one provided when
+ * the task was registered.
+ * @param {boolean} aForce
+ * For debugging, forces the task to run even if it has run in the last
+ * 24 hours, and forces the notification to show.
+ */
+ void doTask(in AString aUniqueToken, in boolean aForce);
+
+ /**
* Checks that the main app ran recently.
*
* @return {boolean} true if the app ran recently.
--
2.11.4.GIT