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 +using namespace Microsoft::WRL; +using namespace ABI::Windows::Data::Xml::Dom; +using namespace ABI::Windows::Foundation; +using namespace ABI::Windows::UI::Notifications; +using namespace Windows::Foundation; + + +namespace WinToastLib { + + class IWinToastHandler { + public: + enum WinToastDismissalReason { + UserCanceled = ToastDismissalReason::ToastDismissalReason_UserCanceled, + ApplicationHidden = ToastDismissalReason::ToastDismissalReason_ApplicationHidden, + TimedOut = ToastDismissalReason::ToastDismissalReason_TimedOut + }; + virtual ~IWinToastHandler() = default; + virtual void toastActivated() const = 0; + virtual void toastActivated(int actionIndex) const = 0; + virtual void toastDismissed(WinToastDismissalReason state) const = 0; + virtual void toastFailed() const = 0; + }; + + 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 }; + enum WinToastTemplateType { + ImageAndText01 = ToastTemplateType::ToastTemplateType_ToastImageAndText01, + ImageAndText02 = ToastTemplateType::ToastTemplateType_ToastImageAndText02, + ImageAndText03 = ToastTemplateType::ToastTemplateType_ToastImageAndText03, + ImageAndText04 = ToastTemplateType::ToastTemplateType_ToastImageAndText04, + Text01 = ToastTemplateType::ToastTemplateType_ToastText01, + Text02 = ToastTemplateType::ToastTemplateType_ToastText02, + Text03 = ToastTemplateType::ToastTemplateType_ToastText03, + Text04 = ToastTemplateType::ToastTemplateType_ToastText04, + }; + + enum AudioSystemFile { + DefaultSound, + IM, + Mail, + Reminder, + SMS, + Alarm, + Alarm2, + Alarm3, + Alarm4, + Alarm5, + Alarm6, + Alarm7, + Alarm8, + Alarm9, + Alarm10, + Call, + Call1, + Call2, + Call3, + Call4, + Call5, + Call6, + Call7, + Call8, + Call9, + Call10, + }; + + + WinToastTemplate(_In_ WinToastTemplateType type = WinToastTemplateType::ImageAndText02); + ~WinToastTemplate(); + + void setFirstLine(_In_ const std::wstring& text); + 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 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; + std::size_t actionsCount() const; + bool hasImage() const; + const std::vector& textFields() const; + const std::wstring& textField(_In_ TextField pos) const; + const std::wstring& actionLabel(_In_ std::size_t pos) const; + 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; + Duration duration() const; + private: + std::vector _textFields{}; + std::vector _actions{}; + 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}; + Duration _duration{Duration::System}; + }; + + class WinToast { + public: + enum WinToastError { + NoError = 0, + NotInitialized, + SystemNotSupported, + ShellLinkNotCreated, + InvalidAppUserModelID, + InvalidParameters, + InvalidHandler, + NotDisplayed, + UnknownError + }; + + enum ShortcutResult { + SHORTCUT_UNCHANGED = 0, + SHORTCUT_WAS_CHANGED = 1, + SHORTCUT_WAS_CREATED = 2, + + SHORTCUT_MISSING_PARAMETERS = -1, + SHORTCUT_INCOMPATIBLE_OS = -2, + SHORTCUT_COM_INIT_FAILURE = -3, + 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(); + static bool isCompatible(); + static bool isSupportingModernFeatures(); + static std::wstring configureAUMI(_In_ const std::wstring& companyName, + _In_ const std::wstring& productName, + _In_ const std::wstring& subProduct = std::wstring(), + _In_ const std::wstring& versionInformation = std::wstring()); + static const std::wstring& strerror(_In_ WinToastError error); + virtual bool initialize(_Out_ WinToastError* error = nullptr); + virtual bool isInitialized() const; + virtual bool hideToast(_In_ INT64 id); + virtual INT64 showToast(_In_ const WinToastTemplate& toast, _In_ IWinToastHandler* handler, _Out_ WinToastError* error = nullptr); + virtual void clear(); + virtual enum ShortcutResult createShortcut(); + + const std::wstring& appName() const; + 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{}; + + HRESULT validateShellLinkHelper(_Out_ bool& wasChanged); + HRESULT createShellLinkHelper(); + HRESULT setImageFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& path); + HRESULT setAudioFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& path, _In_opt_ WinToastTemplate::AudioOption option = WinToastTemplate::AudioOption::Default); + HRESULT setTextFieldHelper(_In_ IXmlDocument *xml, _In_ const std::wstring& text, _In_ UINT32 pos); + 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_ WinToastError* error, _In_ WinToastError value); + }; +} +#endif // WINTOASTLIB_H diff --git a/third_party/moz.build b/third_party/moz.build index 27d7794efd0a..abb3aff2c164 100644 --- a/third_party/moz.build +++ b/third_party/moz.build @@ -88,6 +88,9 @@ with Files('rlbox_wasm2c_sandbox/**'): with Files('wasm2c/**'): BUG_COMPONENT = ('Core', 'Security: RLBox') +with Files('WinToast/**'): + BUG_COMPONENT = ('Toolkit', 'General') + with Files('libsrtp/**'): BUG_COMPONENT = ('Core', 'WebRTC: Networking') diff --git a/toolkit/content/license.html b/toolkit/content/license.html index e9d26423541e..6856360a9e0c 100644 --- a/toolkit/content/license.html +++ b/toolkit/content/license.html @@ -153,6 +153,9 @@
  • Validator License
  • VTune License
  • WebRTC License
  • +#ifdef MOZ_DEFAULT_BROWSER_AGENT +
  • WinToast License
  • +#endif
  • x264 License
  • Xiph.org Foundation License
  • @@ -3616,6 +3619,9 @@ SOFTWARE.
  • third_party/rust/synstructure
  • third_party/rust/void
  • js/src/zydis (unless otherwise specified)
  • +#ifdef MOZ_DEFAULT_BROWSER_AGENT +
  • third_party/WinToast unless otherwise specified
  • +#endif See the individual LICENSE files or headers for copyright owners.

    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