diff --git a/dev/AppNotifications/AppNotificationManager.cpp b/dev/AppNotifications/AppNotificationManager.cpp index 3aab041279..13591aae0b 100644 --- a/dev/AppNotifications/AppNotificationManager.cpp +++ b/dev/AppNotifications/AppNotificationManager.cpp @@ -21,6 +21,7 @@ #include #include #include +#include using namespace std::literals; @@ -150,6 +151,54 @@ namespace winrt::Microsoft::Windows::AppNotifications::implementation } } + void AppNotificationManager::Register(hstring const& displayName, winrt::Windows::Foundation::Uri const& iconUri) + { + if (!IsSupported()) + { + return; + } + + HRESULT hr{ S_OK }; + + auto logTelemetry{ wil::scope_exit([&]() { + AppNotificationTelemetry::LogRegister(hr, m_appId); + }) }; + + try + { + THROW_HR_IF_MSG(E_ILLEGAL_METHOD_CALL, AppModel::Identity::IsPackagedProcess(), "Not applicable for packaged applications"); + + THROW_HR_IF(E_INVALIDARG, displayName.empty() || (iconUri == nullptr)); + + AppNotificationAssets assets{ ValidateAssets(displayName, iconUri.RawUri().c_str()) }; + + { + auto lock{ m_lock.lock_exclusive() }; + THROW_HR_IF_MSG(HRESULT_FROM_WIN32(ERROR_OPERATION_IN_PROGRESS), m_registering, "Registration is in progress!"); + m_registering = true; + } + + auto registeringScopeExit{ wil::scope_exit([&]() + { + auto lock { m_lock.lock_exclusive() }; + m_registering = false; + }) }; + + winrt::guid registeredClsid{ RegisterUnpackagedApp(assets) }; + + // Create event handle before COM Registration otherwise if a notification arrives will lead to race condition + m_waitHandleForArgs.create(); + + // Register the AppNotificationManager as a COM server for Shell to Activate and Invoke + RegisterComServer(registeredClsid); + } + catch (...) + { + hr = wil::ResultFromCaughtException(); + throw; + } + } + void AppNotificationManager::RegisterComServer(winrt::guid const& registeredClsid) { auto lock{ m_lock.lock_exclusive() }; diff --git a/dev/AppNotifications/AppNotificationManager.h b/dev/AppNotifications/AppNotificationManager.h index eb0cd938fc..422576fa19 100644 --- a/dev/AppNotifications/AppNotificationManager.h +++ b/dev/AppNotifications/AppNotificationManager.h @@ -20,6 +20,7 @@ namespace winrt::Microsoft::Windows::AppNotifications::implementation static winrt::Microsoft::Windows::AppNotifications::AppNotificationManager Default(); static winrt::Windows::Foundation::IInspectable AppNotificationDeserialize(winrt::Windows::Foundation::Uri const& uri); void Register(); + void Register(hstring const& displayName, winrt::Windows::Foundation::Uri const& iconUri); void Unregister(); void UnregisterAll(); static bool IsSupported(); diff --git a/dev/AppNotifications/AppNotificationUtility.cpp b/dev/AppNotifications/AppNotificationUtility.cpp index c21127e86e..4313530603 100644 --- a/dev/AppNotifications/AppNotificationUtility.cpp +++ b/dev/AppNotifications/AppNotificationUtility.cpp @@ -286,6 +286,15 @@ AppNotificationAssets Microsoft::Windows::AppNotifications::Helpers::GetAssets() return assets; } +AppNotificationAssets Microsoft::Windows::AppNotifications::Helpers::ValidateAssets(winrt::hstring const& displayName, std::filesystem::path const& iconFilePath) +{ + winrt::check_bool(std::filesystem::exists(iconFilePath)); + + THROW_HR_IF_MSG(E_INVALIDARG, !IsIconFileExtensionSupported(iconFilePath), "Icon format not supported"); + + return AppNotificationAssets{ displayName.c_str(), iconFilePath.wstring() }; +} + void Microsoft::Windows::AppNotifications::Helpers::RegisterAssets(std::wstring const& appId, std::wstring const& clsid, AppNotificationAssets const& assets) { wil::unique_hkey hKey; diff --git a/dev/AppNotifications/AppNotificationUtility.h b/dev/AppNotifications/AppNotificationUtility.h index 11dd6649a9..64e92f7fee 100644 --- a/dev/AppNotifications/AppNotificationUtility.h +++ b/dev/AppNotifications/AppNotificationUtility.h @@ -65,4 +65,6 @@ namespace Microsoft::Windows::AppNotifications::Helpers std::wstring GetDisplayNameBasedOnProcessName(); Microsoft::Windows::AppNotifications::ShellLocalization::AppNotificationAssets GetAssets(); + + Microsoft::Windows::AppNotifications::ShellLocalization::AppNotificationAssets ValidateAssets(winrt::hstring const& displayName, std::filesystem::path const& iconFilePath); } diff --git a/dev/AppNotifications/AppNotifications.idl b/dev/AppNotifications/AppNotifications.idl index f48f85b413..4b33e18677 100644 --- a/dev/AppNotifications/AppNotifications.idl +++ b/dev/AppNotifications/AppNotifications.idl @@ -4,7 +4,7 @@ import "..\AppLifecycle\AppLifecycle.idl"; namespace Microsoft.Windows.AppNotifications { - [contractversion(1)] + [contractversion(2)] apicontract AppNotificationsContract {} // Event args for the Notification Activation @@ -123,6 +123,10 @@ namespace Microsoft.Windows.AppNotifications // For Unpackaged apps, the caller process will be registered as the COM server. And assets like displayname and icon will be gleaned from Shell and registered as well. void Register(); + // Unpackaged Apps can call this API to register custom displayname and icon for AppNotifications and register themselves as a COM server. + [contract(AppNotificationsContract, 2)] + void Register(String displayName, Windows.Foundation.Uri iconUri); + // Unregisters the COM Service so that a subsequent activation will launch a new process void Unregister(); diff --git a/dev/AppNotifications/ShellLocalization.cpp b/dev/AppNotifications/ShellLocalization.cpp index 764e501afb..8c6061dd28 100644 --- a/dev/AppNotifications/ShellLocalization.cpp +++ b/dev/AppNotifications/ShellLocalization.cpp @@ -167,7 +167,8 @@ void WriteHIconToPngFile(wil::unique_hicon const& hIcon, _In_ PCWSTR pszFileName THROW_IF_FAILED(spStreamOut->Commit(STGC_DANGEROUSLYCOMMITMERELYTODISKCACHE)); } -bool IsIconFileExtensionSupported(std::filesystem::path const& iconFilePath) + +bool Microsoft::Windows::AppNotifications::ShellLocalization::IsIconFileExtensionSupported(std::filesystem::path const& iconFilePath) { static PCWSTR c_supportedExtensions[]{ L".bmp", L".ico", L".jpg", L".png" }; @@ -258,10 +259,7 @@ HRESULT Microsoft::Windows::AppNotifications::ShellLocalization::DeleteIconFromC std::path iconFilePath{ RetrieveLocalFolderPath() / (notificationAppId + c_pngExtension) }; // If DeleteFile returned FALSE, then deletion failed and we should return the corresponding error code. - if (DeleteFile(iconFilePath.c_str()) == FALSE) - { - THROW_HR(HRESULT_FROM_WIN32(GetLastError())); - } + RETURN_IF_WIN32_BOOL_FALSE(DeleteFileW(iconFilePath.c_str())); return S_OK; } diff --git a/dev/AppNotifications/ShellLocalization.h b/dev/AppNotifications/ShellLocalization.h index 5dc924f6c0..5d845f1ac1 100644 --- a/dev/AppNotifications/ShellLocalization.h +++ b/dev/AppNotifications/ShellLocalization.h @@ -22,4 +22,6 @@ namespace Microsoft::Windows::AppNotifications::ShellLocalization HRESULT RetrieveAssetsFromShortcut(_Out_ Microsoft::Windows::AppNotifications::ShellLocalization::AppNotificationAssets& assets) noexcept; HRESULT DeleteIconFromCache() noexcept; + + bool IsIconFileExtensionSupported(std::filesystem::path const& iconFilePath); } diff --git a/specs/AppNotifications/AppNotifications-spec.md b/specs/AppNotifications/AppNotifications-spec.md index 23fb67a447..0537beea9b 100644 --- a/specs/AppNotifications/AppNotifications-spec.md +++ b/specs/AppNotifications/AppNotifications-spec.md @@ -139,6 +139,53 @@ int main() } ``` +## Registering for App Notifications using assets + +For Unpackaged applications, the developer can Register using a custom Display Name and Icon. +WinAppSDK will register the application and display these assets when an App Notification is received. +The developer should provide both the assets or not provide them at all. Icon provided by the developer +should be a valid supported format. The API supports the formats - png, bmp, jpg, ico. The icon +should reside on the local machine only otherwise the API throws an exception. For Packaged applications, +this API is not applicable and will throw an exception. Below are some examples of usage: + +```cpp +int main() +{ + auto manager = winrt::AppNotificationManager::Default(); + + std::wstring iconFilepath{ std::filesystem::current_path() / "icon.ico" }; + winrt::hstring displayName{ L"AppNotifications" }; + + manager.Register(displayName, winrt::Windows::Foundation::Uri {iconFilepath}); + + // other app init and then message loop here + + // Call Unregister() before exiting main so that subsequent invocations will launch a new process + manager.Unregister(); + return 0; +} +``` + +```cpp +int main() +{ + auto manager = winrt::AppNotificationManager::Default(); + + std::wstring iconFilepath{ std::filesystem::current_path() / "icon.ico" }; + + std::wstring displayName{}; + wil::GetModuleFileNameExW(GetCurrentProcess(), nullptr, displayName); + + manager.Register(displayName.c_str(), winrt::Windows::Foundation::Uri {iconFilepath}); + + // other app init and then message loop here + + // Call Unregister() before exiting main so that subsequent invocations will launch a new process + manager.Unregister(); + return 0; +} +``` + ## Displaying an App Notification To display a Notification, an app needs to define a payload in xml. In the example below, the @@ -451,6 +498,9 @@ namespace Microsoft.Windows.AppNotifications // For Unpackaged apps, the caller process will be registered as the COM server. And assets like displayname and icon will be gleaned from Shell and registered as well. void Register(); + // For Unpackaged apps only, the caller process will be registered as the COM server. + void Register(String displayName, Windows.Foundation.Uri iconUri); + // Unregisters the COM Service so that a subsequent activation will launch a new process void Unregister(); diff --git a/test/TestApps/ToastNotificationsTestApp/main.cpp b/test/TestApps/ToastNotificationsTestApp/main.cpp index 938f57f834..5a9ee51151 100644 --- a/test/TestApps/ToastNotificationsTestApp/main.cpp +++ b/test/TestApps/ToastNotificationsTestApp/main.cpp @@ -26,6 +26,7 @@ namespace winrt const std::wstring c_localWindowsAppSDKFolder{ LR"(\Microsoft\WindowsAppSDK\)" }; const std::wstring c_pngExtension{ LR"(.png)" }; const std::wstring c_appUserModelId{ LR"(TaefTestAppId)" }; +const std::wstring c_iconFilepath{ std::filesystem::current_path() / "icon1.ico" }; bool BackgroundActivationTest() // Activating application for background test. { @@ -1324,6 +1325,122 @@ bool VerifyIconPathExists_Unpackaged() return true; } +bool VerifyRegisterWithNullDisplayNameFail_Unpackaged() +{ + // Register is already called in main with an explicit appusermodelId + winrt::AppNotificationManager::Default().UnregisterAll(); + try + { + winrt::AppNotificationManager::Default().Register(winrt::hstring{}, winrt::Uri{ c_iconFilepath }); + } + catch (...) + { + return winrt::to_hresult() == E_INVALIDARG; + } + + return false; +} + +bool VerifyRegisterWithNullIconFail_Unpackaged() +{ + // Register is already called in main with an explicit appusermodelId + winrt::AppNotificationManager::Default().UnregisterAll(); + try + { + winrt::AppNotificationManager::Default().Register(L"AppNotificationApp", nullptr); + } + catch (...) + { + return winrt::to_hresult() == E_INVALIDARG; + } + + return false; +} + +bool VerifyRegisterWithNullDisplayNameAndNullIconFail_Unpackaged() +{ + // Register is already called in main with an explicit appusermodelId + winrt::AppNotificationManager::Default().UnregisterAll(); + try + { + winrt::AppNotificationManager::Default().Register(winrt::hstring{}, nullptr); + } + catch (...) + { + return winrt::to_hresult() == E_INVALIDARG; + } + + return true; +} + +bool VerifyShowToastWithCustomDisplayNameAndIcon_Unpackaged() +{ + // Register is already called in main with an explicit appusermodelId + winrt::AppNotificationManager::Default().UnregisterAll(); + try + { + winrt::AppNotificationManager::Default().Register(L"AppNotificationApp", winrt::Uri{ c_iconFilepath }); + + winrt::check_bool(VerifyShowToast_Unpackaged()); + } + catch (...) + { + return false; + } + + return true; +} + +bool VerifyRegisterWithDisplayNameAndInvalidIconPathFail_Unpackaged() +{ + // Register is already called in main with an explicit appusermodelId + winrt::AppNotificationManager::Default().UnregisterAll(); + try + { + winrt::AppNotificationManager::Default().Register(L"AppNotificationApp", winrt::Uri{ LR"(C:\InvalidPath\)" }); + } + catch (...) + { + return winrt::to_hresult() == HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND); + } + + return false; +} + +bool VerifyRegisterWithEmptyDisplayNameFail_Unpackaged() +{ + // Register is already called in main with an explicit appusermodelId + winrt::AppNotificationManager::Default().UnregisterAll(); + try + { + // hstring treats L"" as assigning nullptr + winrt::AppNotificationManager::Default().Register(L"", winrt::Uri{ c_iconFilepath }); + } + catch (...) + { + return winrt::to_hresult() == E_INVALIDARG; + } + + return false; +} + +bool VerifyRegisterWithAssetsFail() +{ + // Register is already called in main with an explicit appusermodelId + winrt::AppNotificationManager::Default().UnregisterAll(); + try + { + // API fails for Packaged Scenario + winrt::AppNotificationManager::Default().Register(L"AppNotificationApp", winrt::Uri{ LR"(C:\InvalidPath\)" }); + } + catch (...) + { + return winrt::to_hresult() == E_ILLEGAL_METHOD_CALL; + } + + return false; +} + std::map const& GetSwitchMapping() { static std::map switchMapping = { @@ -1382,6 +1499,14 @@ std::map const& GetSwitchMapping() { "VerifyToastProgressDataSequence0Fail", &VerifyToastProgressDataSequence0Fail }, { "VerifyToastUpdateZeroSequenceFail_Unpackaged", &VerifyToastUpdateZeroSequenceFail_Unpackaged }, { "VerifyIconPathExists_Unpackaged", &VerifyIconPathExists_Unpackaged}, + + { "VerifyRegisterWithNullDisplayNameFail_Unpackaged", &VerifyRegisterWithNullDisplayNameFail_Unpackaged}, + { "VerifyRegisterWithNullIconFail_Unpackaged", &VerifyRegisterWithNullIconFail_Unpackaged}, + { "VerifyRegisterWithNullDisplayNameAndNullIconFail_Unpackaged", &VerifyRegisterWithNullDisplayNameAndNullIconFail_Unpackaged}, + { "VerifyShowToastWithCustomDisplayNameAndIcon_Unpackaged", &VerifyShowToastWithCustomDisplayNameAndIcon_Unpackaged}, + { "VerifyRegisterWithDisplayNameAndInvalidIconPathFail_Unpackaged", &VerifyRegisterWithDisplayNameAndInvalidIconPathFail_Unpackaged}, + { "VerifyRegisterWithEmptyDisplayNameFail_Unpackaged", &VerifyRegisterWithEmptyDisplayNameFail_Unpackaged}, + { "VerifyRegisterWithAssetsFail", &VerifyRegisterWithAssetsFail}, }; return switchMapping; diff --git a/test/ToastNotificationTests/APITests.cpp b/test/ToastNotificationTests/APITests.cpp index 14c20dd2cb..73a58a435e 100644 --- a/test/ToastNotificationTests/APITests.cpp +++ b/test/ToastNotificationTests/APITests.cpp @@ -583,5 +583,40 @@ namespace Test::ToastNotifications { RunTestUnpackaged(L"VerifyIconPathExists_Unpackaged", testWaitTime()); } + + TEST_METHOD(VerifyRegisterWithNullDisplayNameFail_Unpackaged) + { + RunTestUnpackaged(L"VerifyRegisterWithNullDisplayNameFail_Unpackaged", testWaitTime()); + } + + TEST_METHOD(VerifyRegisterWithNullIconFail_Unpackaged) + { + RunTestUnpackaged(L"VerifyRegisterWithNullIconFail_Unpackaged", testWaitTime()); + } + + TEST_METHOD(VerifyRegisterWithNullDisplayNameAndNullIconFail_Unpackaged) + { + RunTestUnpackaged(L"VerifyRegisterWithNullDisplayNameAndNullIconFail_Unpackaged", testWaitTime()); + } + + TEST_METHOD(VerifyShowToastWithCustomDisplayNameAndIcon_Unpackaged) + { + RunTestUnpackaged(L"VerifyShowToastWithCustomDisplayNameAndIcon_Unpackaged", testWaitTime()); + } + + TEST_METHOD(VerifyRegisterWithDisplayNameAndInvalidIconPathFail_Unpackaged) + { + RunTestUnpackaged(L"VerifyRegisterWithDisplayNameAndInvalidIconPathFail_Unpackaged", testWaitTime()); + } + + TEST_METHOD(VerifyRegisterWithEmptyDisplayNameFail_Unpackaged) + { + RunTestUnpackaged(L"VerifyRegisterWithEmptyDisplayNameFail_Unpackaged", testWaitTime()); + } + + TEST_METHOD(VerifyRegisterWithAssetsFail) + { + RunTest(L"VerifyRegisterWithAssetsFail", testWaitTime()); + } }; }