diff --git a/WindowsAppRuntime.sln b/WindowsAppRuntime.sln index 995bff0f60..095ea6a6a1 100644 --- a/WindowsAppRuntime.sln +++ b/WindowsAppRuntime.sln @@ -293,6 +293,13 @@ EndProject Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "AccessControlTestAppPackage", "test\TestApps\AccessControlTestAppPackage\AccessControlTestAppPackage.wapproj", "{03EBF097-66C6-4996-95A3-28F6F5999E27}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ToastNotificationsDemoApp", "test\TestApps\ToastNotificationsDemoApp\ToastNotificationsDemoApp.vcxproj", "{412D023E-8635-4AD2-A0EA-E19E08D36915}" + ProjectSection(ProjectDependencies) = postProject + {B73AD907-6164-4294-88FB-F3C9C10DA1F1} = {B73AD907-6164-4294-88FB-F3C9C10DA1F1} + {A7391725-4EF5-438F-8DE1-645423E46955} = {A7391725-4EF5-438F-8DE1-645423E46955} + {9C1A6C58-52D6-4514-9120-5C339C5DF4BE} = {9C1A6C58-52D6-4514-9120-5C339C5DF4BE} + {B71E818A-882E-456A-87E5-4DE4A6602B99} = {B71E818A-882E-456A-87E5-4DE4A6602B99} + {C422B090-F501-4204-8068-1084B0799405} = {C422B090-F501-4204-8068-1084B0799405} + EndProjectSection EndProject Project("{C7167F0D-BC9F-4E6E-AFE1-012C56B48DB5}") = "ToastNotificationsDemoAppPackage", "test\TestApps\ToastNotificationsDemoAppPackage\ToastNotificationsDemoAppPackage.wapproj", "{E695A08E-8735-41CD-AE55-A5B589BA297F}" EndProject @@ -301,6 +308,7 @@ EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "AppNotifications", "dev\AppNotifications\AppNotifications.vcxitems", "{B4824897-88E0-4927-8FB9-E60106F01ED9}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Windows.AppNotifications.Projection", "dev\Projections\CS\Microsoft.Windows.AppNotifications.Projection\Microsoft.Windows.AppNotifications.Projection.csproj", "{2385A443-A1C2-4562-8D28-D0AD239EE5E7}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{82A73181-EA4A-431A-B82B-BE6734604CC9}" EndProject Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "Test_Common", "test\Common\Common.vcxproj", "{50451390-66E7-4465-8804-427560625794}" @@ -1270,10 +1278,6 @@ Global {BC5E5A3E-E733-4388-8B00-F8495DA7C778} = {3DE93B2F-F887-437D-B512-6B1024ABA290} {34671779-4A4D-4D0E-B259-CD0F14D4F6D4} = {448ED2E5-0B37-4D97-9E6B-8C10A507976A} {885A43FA-052D-4B0D-A2DC-13EE15796435} = {34671779-4A4D-4D0E-B259-CD0F14D4F6D4} - {BC5E5A3E-E733-4388-8B00-F8495DA7C778} = {3DE93B2F-F887-437D-B512-6B1024ABA290} - {BC5E5A3E-E733-4388-8B00-F8495DA7C778} = {3DE93B2F-F887-437D-B512-6B1024ABA290} - {34671779-4A4D-4D0E-B259-CD0F14D4F6D4} = {448ED2E5-0B37-4D97-9E6B-8C10A507976A} - {885A43FA-052D-4B0D-A2DC-13EE15796435} = {34671779-4A4D-4D0E-B259-CD0F14D4F6D4} {A06B7FE8-D201-4EA2-BB10-61B4595EC377} = {448ED2E5-0B37-4D97-9E6B-8C10A507976A} {C91BCB93-9ED1-4ACD-85F3-26F9F6AC52E3} = {A06B7FE8-D201-4EA2-BB10-61B4595EC377} {08BC78E0-63C6-49A7-81B3-6AFC3DEAC4DE} = {8630F7AA-2969-4DC9-8700-9B468C1DC21D} @@ -1284,8 +1288,6 @@ Global {1C9A0791-2BAA-420B-84B6-C0721F22A6E8} = {448ED2E5-0B37-4D97-9E6B-8C10A507976A} {B4824897-88E0-4927-8FB9-E60106F01ED9} = {1C9A0791-2BAA-420B-84B6-C0721F22A6E8} {2385A443-A1C2-4562-8D28-D0AD239EE5E7} = {716C26A0-E6B0-4981-8412-D14A4D410531} - {34671779-4A4D-4D0E-B259-CD0F14D4F6D4} = {448ED2E5-0B37-4D97-9E6B-8C10A507976A} - {885A43FA-052D-4B0D-A2DC-13EE15796435} = {34671779-4A4D-4D0E-B259-CD0F14D4F6D4} {82A73181-EA4A-431A-B82B-BE6734604CC9} = {8630F7AA-2969-4DC9-8700-9B468C1DC21D} {50451390-66E7-4465-8804-427560625794} = {82A73181-EA4A-431A-B82B-BE6734604CC9} {0419CA2B-5ED1-49F0-B70B-5F470A15D3D0} = {448ED2E5-0B37-4D97-9E6B-8C10A507976A} diff --git a/dev/AppNotifications/AppNotificationManager.cpp b/dev/AppNotifications/AppNotificationManager.cpp index c5099d0b3d..5915e63c28 100644 --- a/dev/AppNotifications/AppNotificationManager.cpp +++ b/dev/AppNotifications/AppNotificationManager.cpp @@ -20,6 +20,7 @@ #include #include #include +#include using namespace std::literals; @@ -46,6 +47,7 @@ namespace PushNotificationHelpers } using namespace Microsoft::Windows::AppNotifications::Helpers; +using namespace Microsoft::Windows::AppNotifications::ShellLocalization; namespace winrt::Microsoft::Windows::AppNotifications::implementation { @@ -220,6 +222,12 @@ namespace winrt::Microsoft::Windows::AppNotifications::implementation if (!AppModel::Identity::IsPackagedProcess()) { + // If the app icon was inferred from process, then we should clean it up. + // Do not fail this function if such a file doesn't exist, + // which is the case if the icon was retrieved from shortcut or there is no IconUri in registry. + winrt::hresult deleteIconResult{ DeleteIconFromCache() }; + THROW_HR_IF(deleteIconResult, FAILED(deleteIconResult) && deleteIconResult != HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)); + std::wstring storedComActivatorString; THROW_IF_FAILED(GetActivatorGuid(storedComActivatorString)); UnRegisterComServer(storedComActivatorString); diff --git a/dev/AppNotifications/AppNotificationUtility.cpp b/dev/AppNotifications/AppNotificationUtility.cpp index b0f645b2c4..6d2bc0c942 100644 --- a/dev/AppNotifications/AppNotificationUtility.cpp +++ b/dev/AppNotifications/AppNotificationUtility.cpp @@ -4,6 +4,7 @@ #include "pch.h" #include + #include "AppNotificationUtility.h" #include #include @@ -13,11 +14,17 @@ #include "AppNotification.h" #include "NotificationProgressData.h" -#include -#include -#include // PKEY properties -#include // IPropertyStore +#include #include +#include +#include + +#include + +namespace std +{ + using namespace std::filesystem; +} namespace winrt { @@ -106,7 +113,9 @@ void Microsoft::Windows::AppNotifications::Helpers::RegisterComServer(wil::uniqu { wil::unique_hkey hKey; //subKey: Software\Classes\CLSID\{comActivatorGuidString}\LocalServer32 - std::wstring subKey{ c_clsIdPath + clsid.get() + LR"(\LocalServer32)" }; + std::wstring subKey{ c_clsIdPath }; + subKey.append(clsid.get()); + subKey.append(LR"(\LocalServer32)"); THROW_IF_WIN32_ERROR(RegCreateKeyEx( HKEY_CURRENT_USER, @@ -216,7 +225,7 @@ HRESULT Microsoft::Windows::AppNotifications::Helpers::GetActivatorGuid(std::wst } CATCH_RETURN() -std::wstring Microsoft::Windows::AppNotifications::Helpers::SetDisplayNameBasedOnProcessName() +std::wstring Microsoft::Windows::AppNotifications::Helpers::GetDisplayNameBasedOnProcessName() { std::wstring displayName{}; THROW_IF_FAILED(wil::GetModuleFileNameExW(GetCurrentProcess(), nullptr, displayName)); @@ -232,82 +241,6 @@ std::wstring Microsoft::Windows::AppNotifications::Helpers::SetDisplayNameBasedO return displayName; } -void Microsoft::Windows::AppNotifications::Helpers::RegisterAssets(std::wstring const& appId, std::wstring const& clsid) -{ - wil::unique_hkey hKey; - // subKey: \Software\Classes\AppUserModelId\{AppGUID} - std::wstring subKey{ c_appIdentifierPath + appId }; - - THROW_IF_WIN32_ERROR(RegCreateKeyEx( - HKEY_CURRENT_USER, - subKey.c_str(), - 0, - nullptr /* lpClass */, - REG_OPTION_NON_VOLATILE, - KEY_ALL_ACCESS, - nullptr /* lpSecurityAttributes */, - &hKey, - nullptr /* lpdwDisposition */)); - - // Retrieve the display name - std::wstring displayName{}; - displayName = SetDisplayNameBasedOnProcessName(); - - // Retrieve the icon - std::wstring iconFilePath{}; - - wil::unique_hwnd hWindow{ GetConsoleWindow() }; - - if (hWindow) - { - // Retrieve DisplayName and IconUri - // - DisplayName: Retrieve from Shell. If not specified, fall back to filename. - // - Icon: Retrieve from Shell. If it's not the case or the file extension is unsupported, then throw. - winrt::com_ptr propertyStore; - THROW_IF_FAILED(SHGetPropertyStoreForWindow(hWindow.get(), IID_PPV_ARGS(propertyStore.put()))); - - wil::unique_prop_variant propVariantDisplayName; - // Do not throw in case of failure, default to the filepath approach below as fallback to set a DisplayName. - THROW_IF_FAILED(propertyStore->GetValue(PKEY_AppUserModel_RelaunchDisplayNameResource, &propVariantDisplayName)); - - if (propVariantDisplayName.vt == VT_LPWSTR && propVariantDisplayName.pwszVal != nullptr && wcslen(propVariantDisplayName.pwszVal) > 0) - { - displayName = propVariantDisplayName.pwszVal; - } - - wil::unique_prop_variant propVariantIcon; - THROW_IF_FAILED(propertyStore->GetValue(PKEY_AppUserModel_RelaunchIconResource, &propVariantIcon)); - - THROW_HR_IF_MSG(E_UNEXPECTED, (propVariantIcon.vt == VT_EMPTY || propVariantIcon.pwszVal == nullptr), "Icon is not specified"); - - THROW_HR_IF_MSG(E_UNEXPECTED, propVariantIcon.vt != VT_LPWSTR, "Icon should be a valid Unicode string"); - - THROW_HR_IF_MSG(E_UNEXPECTED, wcslen(propVariantIcon.pwszVal) == 0, "Icon is an empty string"); - - iconFilePath = propVariantIcon.pwszVal; - - // Icon filepaths from Shell APIs have this format: ,-, - // since .ico files can have multiple icons in the same file. - // NotificationController doesn't seem to support such format, so let it take the first icon by default. - auto iteratorForCommaDelimiter{ iconFilePath.find_first_of(L",") }; - if (iteratorForCommaDelimiter != std::wstring::npos) // It may or may not have an index, which is fine. - { - iconFilePath.erase(iteratorForCommaDelimiter); - } - - auto iteratorForFileExtension{ iconFilePath.find_first_of(L".") }; - THROW_HR_IF_MSG(E_UNEXPECTED, iteratorForFileExtension == std::wstring::npos, "You must provide a valid filepath as the app icon."); - - std::wstring iconFileExtension{ iconFilePath.substr(iteratorForFileExtension) }; - THROW_HR_IF_MSG(E_UNEXPECTED, iconFileExtension != L".ico" && iconFileExtension != L".png", - "You must provide a supported file extension as the icon (.ico or .png)."); - } - - RegisterValue(hKey, L"DisplayName", reinterpret_cast(displayName.c_str()), REG_EXPAND_SZ, displayName.size() * sizeof(wchar_t)); - RegisterValue(hKey, L"IconUri", reinterpret_cast(iconFilePath.c_str()), REG_EXPAND_SZ, iconFilePath.size() * sizeof(wchar_t)); - RegisterValue(hKey, L"CustomActivator", reinterpret_cast(clsid.c_str()), REG_SZ, clsid.size() * sizeof(wchar_t)); -} - winrt::guid Microsoft::Windows::AppNotifications::Helpers::RegisterComActivatorGuidAndAssets() { std::wstring registeredGuid; @@ -331,12 +264,55 @@ winrt::guid Microsoft::Windows::AppNotifications::Helpers::RegisterComActivatorG THROW_IF_FAILED(hr); } - std::wstring notificationAppId{ RetrieveNotificationAppId() }; + std::wstring notificationAppId{ Microsoft::Windows::AppNotifications::Helpers::RetrieveNotificationAppId() }; RegisterAssets(notificationAppId, registeredGuid); // Remove braces around the guid string return winrt::guid(registeredGuid.substr(1, registeredGuid.size() - 2)); - +} + +void Microsoft::Windows::AppNotifications::Helpers::RegisterAssets(std::wstring const& appId, std::wstring const& clsid) +{ + wil::unique_hkey hKey; + // subKey: \Software\Classes\AppUserModelId\{AppGUID} + std::wstring subKey{ c_appIdentifierPath + appId }; + + THROW_IF_WIN32_ERROR(RegCreateKeyEx( + HKEY_CURRENT_USER, + subKey.c_str(), + 0, + nullptr /* lpClass */, + REG_OPTION_NON_VOLATILE, + KEY_ALL_ACCESS, + nullptr /* lpSecurityAttributes */, + &hKey, + nullptr /* lpdwDisposition */)); + + // Try the following techniques to retrieve display name and icon: + // 1. Based on the best app shortcut, using the FrameworkUdk. + // 2. From the current process. + // 3. Set a default DisplayName, but leave empty the icon file path so Shell can set a default icon. + Microsoft::Windows::AppNotifications::ShellLocalization::AppNotificationAssets assets{}; + + if (FAILED(Microsoft::Windows::AppNotifications::ShellLocalization::RetrieveAssetsFromShortcut(assets)) && + FAILED(Microsoft::Windows::AppNotifications::ShellLocalization::RetrieveAssetsFromProcess(assets))) + { + assets.displayName = GetDisplayNameBasedOnProcessName(); + } + + RegisterValue(hKey, L"DisplayName", reinterpret_cast(assets.displayName.c_str()), REG_EXPAND_SZ, assets.displayName.size() * sizeof(wchar_t)); + + // If no icon is specified in the Registry, the OS will render a default icon for App Notifications. + if (!assets.iconFilePath.empty()) + { + RegisterValue(hKey, L"IconUri", reinterpret_cast(assets.iconFilePath.c_str()), REG_EXPAND_SZ, assets.iconFilePath.size() * sizeof(wchar_t)); + } + else // clean any Icon URI from previous registrations + { + LOG_IF_WIN32_ERROR(RegDeleteValue(hKey.get(), L"IconUri")); // Do not throw, since this is a best effort action. + } + + RegisterValue(hKey, L"CustomActivator", reinterpret_cast(clsid.c_str()), REG_SZ, wil::guid_string_length * sizeof(wchar_t)); } wil::unique_cotaskmem_string Microsoft::Windows::AppNotifications::Helpers::ConvertUtf8StringToWideString(unsigned long length, const byte* utf8String) diff --git a/dev/AppNotifications/AppNotificationUtility.h b/dev/AppNotifications/AppNotificationUtility.h index f865648c38..e5a3537506 100644 --- a/dev/AppNotifications/AppNotificationUtility.h +++ b/dev/AppNotifications/AppNotificationUtility.h @@ -11,10 +11,10 @@ namespace Microsoft::Windows::AppNotifications::Helpers { - const std::wstring c_appIdentifierPath{ LR"(Software\Classes\AppUserModelId\)" }; - const std::wstring c_clsIdPath{ LR"(Software\Classes\CLSID\)" }; - const std::wstring c_quote{ LR"(")" }; - const std::wstring c_notificationActivatedArgument{ L" ----AppNotificationActivated:" }; + const PCWSTR c_appIdentifierPath{ LR"(Software\Classes\AppUserModelId\)" }; + const PCWSTR c_clsIdPath{ LR"(Software\Classes\CLSID\)" }; + const PCWSTR c_quote{ LR"(")" }; + const PCWSTR c_notificationActivatedArgument{ L" ----AppNotificationActivated:" }; inline const int GUID_LENGTH = 39; // GUID + '{' + '}' + '/0' @@ -61,5 +61,5 @@ namespace Microsoft::Windows::AppNotifications::Helpers winrt::Microsoft::Windows::AppNotifications::AppNotification ToastNotificationFromToastProperties(ABI::Microsoft::Internal::ToastNotifications::INotificationProperties* properties); - std::wstring SetDisplayNameBasedOnProcessName(); + std::wstring GetDisplayNameBasedOnProcessName(); } diff --git a/dev/AppNotifications/AppNotifications.vcxitems b/dev/AppNotifications/AppNotifications.vcxitems index cf8a923928..63b054a559 100644 --- a/dev/AppNotifications/AppNotifications.vcxitems +++ b/dev/AppNotifications/AppNotifications.vcxitems @@ -23,6 +23,7 @@ + @@ -32,6 +33,7 @@ + diff --git a/dev/AppNotifications/ShellLocalization.cpp b/dev/AppNotifications/ShellLocalization.cpp new file mode 100644 index 0000000000..0bf96eccec --- /dev/null +++ b/dev/AppNotifications/ShellLocalization.cpp @@ -0,0 +1,259 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#include "pch.h" + +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace std +{ + using namespace std::filesystem; +} + +typedef enum +{ + VERSION1, + VERSION5, +} BITMAP_VERSION; + +winrt::com_ptr ConvertWICBitmapPixelFormat( + winrt::com_ptr const& wicImagingFactory, + winrt::com_ptr const& wicBitmapSource, + WICPixelFormatGUID guidPixelFormatSource, + WICBitmapDitherType bitmapDitherType) +{ + winrt::com_ptr wicFormatConverter; + THROW_IF_FAILED(wicImagingFactory->CreateFormatConverter(wicFormatConverter.put())); + + THROW_IF_FAILED(wicFormatConverter->Initialize(wicBitmapSource.get(), guidPixelFormatSource, bitmapDitherType, nullptr, 0.f, WICBitmapPaletteTypeCustom)); + + // Store the converted bitmap as ppToRenderBitmapSource + winrt::com_ptr wicBitmapSourceConverted; + THROW_IF_FAILED(wicFormatConverter->QueryInterface(_uuidof(IWICBitmapSource), wicBitmapSourceConverted.put_void())); + + return wicBitmapSourceConverted; +} + +void AddFrameToWICBitmap( + winrt::com_ptr const& wicImagingFactory, + winrt::com_ptr const& wicEncoder, + winrt::com_ptr const& wicBitmapSource, + BITMAP_VERSION bitmapVersion) +{ + winrt::com_ptr wicFrameEncoder; + winrt::com_ptr wicEncoderOptions; + + THROW_IF_FAILED(wicEncoder->CreateNewFrame(wicFrameEncoder.put(), wicEncoderOptions.put())); + + GUID containerGuid; + THROW_IF_FAILED(wicEncoder->GetContainerFormat(&containerGuid)); + + if ((containerGuid == GUID_ContainerFormatBmp) && (bitmapVersion == BITMAP_VERSION::VERSION5)) + { + // Write the encoder option to the property bag instance. + VARIANT varValue{}; + varValue.vt = VT_BOOL; + varValue.boolVal = VARIANT_TRUE; + + // Options to enable the v5 header support for 32bppBGRA. + PROPBAG2 v5HeaderOption{}; + v5HeaderOption.pstrName = (LPOLESTR) L"EnableV5Header32bppBGRA"; + + THROW_IF_FAILED(wicEncoderOptions->Write(1, &v5HeaderOption, &varValue)); + } + + THROW_IF_FAILED(wicFrameEncoder->Initialize(wicEncoderOptions.get())); + + UINT width{}; + UINT height{}; + THROW_IF_FAILED(wicBitmapSource->GetSize(&width, &height)); + + THROW_IF_FAILED(wicFrameEncoder->SetSize(width, height)); + + winrt::com_ptr wicbitmapSourceConverted + { ConvertWICBitmapPixelFormat(wicImagingFactory, wicBitmapSource, GUID_WICPixelFormat32bppBGRA, WICBitmapDitherTypeNone) }; + + WICRect rect{ 0 /* x-coordinate */, 0 /* y-coordinate */, static_cast(width), static_cast(height) }; + + // Write the image data and commit + THROW_IF_FAILED(wicFrameEncoder->WriteSource(wicbitmapSourceConverted.get(), &rect)); + THROW_IF_FAILED(wicFrameEncoder->Commit()); +} + +winrt::com_ptr GetStreamOfWICBitmapSource( + winrt::com_ptr const& wicImagingFactory, + winrt::com_ptr const& wicBitmapSource, + REFGUID guidContainerFormat, + BITMAP_VERSION bitmapVersion) +{ + winrt::com_ptr spImageStream; + THROW_IF_FAILED(CreateStreamOnHGlobal(nullptr /* handle */, true /* delete on release */, spImageStream.put())); + + // Create encoder and initialize it + winrt::com_ptr wicEncoder; + THROW_IF_FAILED(wicImagingFactory->CreateEncoder(guidContainerFormat, nullptr, wicEncoder.put())); + THROW_IF_FAILED(wicEncoder->Initialize(spImageStream.get(), WICBitmapEncoderCacheOption::WICBitmapEncoderNoCache)); + + // Add a single frame to the encoder with the Bitmap + AddFrameToWICBitmap(wicImagingFactory, wicEncoder, wicBitmapSource, bitmapVersion); + THROW_IF_FAILED(wicEncoder->Commit()); + + // Seek the stream to the beginning and transfer + const LARGE_INTEGER lnBeginning{}; + THROW_IF_FAILED(spImageStream->Seek(lnBeginning, STREAM_SEEK_SET, nullptr /* new seek pointer */)); + + return spImageStream; +} + +void SaveImageWithWIC( + winrt::com_ptr const& wicImagingFactory, + winrt::com_ptr const& wicBitmapSource, + GUID const& guidContainerFormat, + winrt::com_ptr& pStream) +{ + winrt::com_ptr spImageStream = GetStreamOfWICBitmapSource(wicImagingFactory, wicBitmapSource, guidContainerFormat, BITMAP_VERSION::VERSION1); + + // Seek the stream to the beginning and transfer + LARGE_INTEGER const lnBeginning{ 0 }; + THROW_IF_FAILED(spImageStream->Seek(lnBeginning, STREAM_SEEK_SET, nullptr /* new seek pointer */)); + + ULARGE_INTEGER lnbuffer{ INT_MAX }; + THROW_IF_FAILED(spImageStream->CopyTo(pStream.get(), lnbuffer, nullptr /* pointer to number of bytes read */, nullptr /* pointer to number of bytes written */)); +} + +void WriteHIconToPngFile(wil::unique_hicon const& hIcon, _In_ PCWSTR pszFileName) +{ + auto wicImagingFactory{ winrt::create_instance(CLSID_WICImagingFactory, CLSCTX_INPROC_SERVER) }; + + winrt::com_ptr wicBitmap; + THROW_IF_FAILED(wicImagingFactory->CreateBitmapFromHICON(hIcon.get(), wicBitmap.put())); + + // Create stream to save the HICON to. + winrt::com_ptr spStream; + THROW_IF_FAILED(CreateStreamOnHGlobal(nullptr /* handle */, TRUE, spStream.put())); + + winrt::com_ptr wicBitmapSource; + wicBitmapSource = wicBitmap; + + SaveImageWithWIC(wicImagingFactory, wicBitmapSource, GUID_ContainerFormatPng, spStream); + + // Write out the stream to a file. + winrt::com_ptr spStreamOut; + THROW_IF_FAILED(SHCreateStreamOnFileEx(pszFileName, STGM_CREATE | STGM_WRITE | STGM_SHARE_DENY_WRITE, FILE_ATTRIBUTE_NORMAL, TRUE, nullptr, spStreamOut.put())); + + STATSTG statstg{}; + THROW_IF_FAILED(spStream->Stat(&statstg, STATFLAG_NONAME)); + + THROW_IF_FAILED(IStream_Reset(spStream.get())); + + THROW_IF_FAILED(spStreamOut->SetSize(statstg.cbSize)); + + THROW_IF_FAILED(spStream->CopyTo(spStreamOut.get(), statstg.cbSize, nullptr /* pointer to number of bytes read */, nullptr /* pointer to number of bytes written */)); + + THROW_IF_FAILED(spStreamOut->Commit(STGC_DANGEROUSLYCOMMITMERELYTODISKCACHE)); +} + +bool IsIconFileExtensionSupported(std::filesystem::path const& iconFilePath) +{ + static PCWSTR c_supportedExtensions[]{ L".bmp", L".ico", L".jpg", L".png" }; + + const auto fileExtension{ iconFilePath.extension() }; + const auto extension{ fileExtension.c_str() }; + for (auto supportedExtension : c_supportedExtensions) + { + if (CompareStringOrdinal(extension, -1, supportedExtension, -1, TRUE) == CSTR_EQUAL) + { + return true; + } + } + + return false; +} + +inline std::path RetrieveLocalFolderPath() +{ + wil::unique_cotaskmem_string localAppDataPath; + THROW_IF_FAILED(SHGetKnownFolderPath(FOLDERID_LocalAppData, 0 /* flags */, nullptr /* access token handle */, &localAppDataPath)); + + // path: C:\Users\\AppData\Local\Microsoft\WindowsAppSDK + std::path localFolderPath{ std::wstring(localAppDataPath.get()) + + Microsoft::Windows::AppNotifications::ShellLocalization::c_localMicrosoftFolder + + Microsoft::Windows::AppNotifications::ShellLocalization::c_localWindowsAppSDKFolder }; + + if (!std::exists(localFolderPath)) + { + std::create_directory(localFolderPath); + } + + return localFolderPath; +} + +inline wil::unique_hicon RetrieveIconFromProcess() +{ + std::wstring processPath{}; + THROW_IF_FAILED(wil::GetModuleFileNameExW(GetCurrentProcess(), nullptr, processPath)); + + // Extract the Small icon as it is good enough for an App Notification icon since higher quality doesn't really impact in a substantial way. + wil::unique_hicon hIcon{}; + THROW_HR_IF(E_FAIL, ExtractIconExW(processPath.c_str(), 0 /* index for first resource */, nullptr /* Large Icon */, &hIcon, 1) == 0); + + return hIcon; +} + +HRESULT Microsoft::Windows::AppNotifications::ShellLocalization::RetrieveAssetsFromProcess(_Out_ Microsoft::Windows::AppNotifications::ShellLocalization::AppNotificationAssets& assets) noexcept try +{ + wil::unique_hicon hIcon{ RetrieveIconFromProcess() }; + + std::wstring notificationAppId{ Microsoft::Windows::AppNotifications::Helpers::RetrieveNotificationAppId() }; + + // path: C:\Users\\AppData\Local\Microsoft\WindowsAppSDK\{AppGUID}.png + auto iconFilePath{ RetrieveLocalFolderPath() / (notificationAppId + c_pngExtension) }; + WriteHIconToPngFile(hIcon, iconFilePath.c_str()); + + assets.displayName = Microsoft::Windows::AppNotifications::Helpers::GetDisplayNameBasedOnProcessName(); + assets.iconFilePath = iconFilePath; + + // TODO: In case we are caching icons from uninstalled apps who didn't call UnregisterAll, + // we should make an effort to clean them up in an opportunistic way. + + return S_OK; +} +CATCH_RETURN() + +HRESULT Microsoft::Windows::AppNotifications::ShellLocalization::RetrieveAssetsFromShortcut(_Out_ Microsoft::Windows::AppNotifications::ShellLocalization::AppNotificationAssets& /*assets*/) noexcept +{ + // Do nothing for now. This is just a placeholder while we wait for the FrameworkUdk API + // to get icon file path from shortcut. This API is already implemented but not ready for consumption. + // THROW_HR_IF_MSG(E_UNEXPECTED, IsIconFileExtensionSupported(iconFilePath)); + + return E_NOTIMPL; +} + +HRESULT Microsoft::Windows::AppNotifications::ShellLocalization::DeleteIconFromCache() noexcept try +{ + std::wstring notificationAppId{ Microsoft::Windows::AppNotifications::Helpers::RetrieveNotificationAppId() }; + + // path: C:\Users\\AppData\Local\Microsoft\WindowsAppSDK\{AppGUID}.png + 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 S_OK; +} +CATCH_RETURN() diff --git a/dev/AppNotifications/ShellLocalization.h b/dev/AppNotifications/ShellLocalization.h new file mode 100644 index 0000000000..39add29dbc --- /dev/null +++ b/dev/AppNotifications/ShellLocalization.h @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#pragma once + +#include "pch.h" + +// In this file we define methods that write/return Shell assets, like DisplayName and icon URI. + +namespace Microsoft::Windows::AppNotifications::ShellLocalization +{ + const PCWSTR c_localMicrosoftFolder{ LR"(Microsoft)" }; + const PCWSTR c_localWindowsAppSDKFolder{ LR"(WindowsAppSDK)" }; + const PCWSTR c_pngExtension{ LR"(.png)" }; + + struct AppNotificationAssets { + std::wstring displayName; + std::wstring iconFilePath; + }; + + HRESULT RetrieveAssetsFromProcess(_Out_ Microsoft::Windows::AppNotifications::ShellLocalization::AppNotificationAssets& assets) noexcept; + + HRESULT RetrieveAssetsFromShortcut(_Out_ Microsoft::Windows::AppNotifications::ShellLocalization::AppNotificationAssets& assets) noexcept; + + HRESULT DeleteIconFromCache() noexcept; +} diff --git a/dev/Common/Common.vcxitems b/dev/Common/Common.vcxitems index 2f1c94fdc4..46f65e5fb0 100644 --- a/dev/Common/Common.vcxitems +++ b/dev/Common/Common.vcxitems @@ -21,8 +21,9 @@ + - + \ No newline at end of file diff --git a/dev/Common/Common.vcxitems.filters b/dev/Common/Common.vcxitems.filters index 5ed4b7cd27..1a5018a3a7 100644 --- a/dev/Common/Common.vcxitems.filters +++ b/dev/Common/Common.vcxitems.filters @@ -29,6 +29,9 @@ Header Files + + Header Files + diff --git a/dev/Common/Microsoft.Foundation.String.h b/dev/Common/Microsoft.Foundation.String.h new file mode 100644 index 0000000000..dae9165ae1 --- /dev/null +++ b/dev/Common/Microsoft.Foundation.String.h @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +#ifndef __MICROSOFT_FOUNDATION_STRING_H +#define __MICROSOFT_FOUNDATION_STRING_H + +namespace Microsoft::Foundation::String +{ + /// Return true if the string is nullptr or an empty string (""). + inline bool IsNullOrEmpty(_In_ PCSTR string) + { + return !string || (*string == '\0'); + } + + /// Return true if the string is nullptr or an empty string (""). + inline bool IsNullOrEmpty(_In_ PCWSTR string) + { + return !string || (*string == L'\0'); + } +} + +#endif // __MICROSOFT_FOUNDATION_STRING_H diff --git a/test/TestApps/ToastNotificationsDemoApp/ToastNotificationsDemoApp.rc b/test/TestApps/ToastNotificationsDemoApp/ToastNotificationsDemoApp.rc new file mode 100644 index 0000000000..49e9d07abb --- /dev/null +++ b/test/TestApps/ToastNotificationsDemoApp/ToastNotificationsDemoApp.rc @@ -0,0 +1,71 @@ +// Microsoft Visual C++ generated resource script. +// +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +#pragma code_page(1252) + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_ICON1 ICON "icon1.ico" + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED + diff --git a/test/TestApps/ToastNotificationsDemoApp/ToastNotificationsDemoApp.vcxproj b/test/TestApps/ToastNotificationsDemoApp/ToastNotificationsDemoApp.vcxproj index a823e15979..978fb56c26 100644 --- a/test/TestApps/ToastNotificationsDemoApp/ToastNotificationsDemoApp.vcxproj +++ b/test/TestApps/ToastNotificationsDemoApp/ToastNotificationsDemoApp.vcxproj @@ -241,6 +241,7 @@ + @@ -267,6 +268,12 @@ {f76b776e-86f5-48c5-8fc7-d2795ecc9746} + + + + + + @@ -285,4 +292,4 @@ - \ No newline at end of file + diff --git a/test/TestApps/ToastNotificationsDemoApp/ToastNotificationsDemoApp.vcxproj.filters b/test/TestApps/ToastNotificationsDemoApp/ToastNotificationsDemoApp.vcxproj.filters index a8a65633bf..38477c94c8 100644 --- a/test/TestApps/ToastNotificationsDemoApp/ToastNotificationsDemoApp.vcxproj.filters +++ b/test/TestApps/ToastNotificationsDemoApp/ToastNotificationsDemoApp.vcxproj.filters @@ -14,4 +14,33 @@ rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms - \ No newline at end of file + + + Source Files + + + Source Files + + + + + Header Files + + + Header Files + + + + + + + + Resource Files + + + + + Resource Files + + + diff --git a/test/TestApps/ToastNotificationsDemoApp/icon1.ico b/test/TestApps/ToastNotificationsDemoApp/icon1.ico new file mode 100644 index 0000000000..5d06b9f285 Binary files /dev/null and b/test/TestApps/ToastNotificationsDemoApp/icon1.ico differ diff --git a/test/TestApps/ToastNotificationsDemoApp/main.cpp b/test/TestApps/ToastNotificationsDemoApp/main.cpp index b04cd19451..b44277eef9 100644 --- a/test/TestApps/ToastNotificationsDemoApp/main.cpp +++ b/test/TestApps/ToastNotificationsDemoApp/main.cpp @@ -133,33 +133,6 @@ bool PostToastHelper(std::wstring const& tag, std::wstring const& group) return true; } -// This function is intended to be called in the unpackaged scenario. -void SetDisplayNameAndIcon() noexcept try -{ - // Not mandatory, but it's highly recommended to specify AppUserModelId - THROW_IF_FAILED(SetCurrentProcessExplicitAppUserModelID(L"TestAppId")); - - // Icon is mandatory - winrt::com_ptr propertyStore; - wil::unique_hwnd hWindow{ GetConsoleWindow() }; - - THROW_IF_FAILED(SHGetPropertyStoreForWindow(hWindow.get(), IID_PPV_ARGS(&propertyStore))); - - wil::unique_prop_variant propVariantIcon; - wil::unique_cotaskmem_string sth = wil::make_unique_string(LR"(%SystemRoot%\system32\@WLOGO_96x96.png)"); - propVariantIcon.pwszVal = sth.release(); - propVariantIcon.vt = VT_LPWSTR; - THROW_IF_FAILED(propertyStore->SetValue(PKEY_AppUserModel_RelaunchIconResource, propVariantIcon)); - - // App name is not mandatory, but it's highly recommended to specify it - wil::unique_prop_variant propVariantAppName; - wil::unique_cotaskmem_string prodName = wil::make_unique_string(L"The Toast Demo App"); - propVariantAppName.pwszVal = prodName.release(); - propVariantAppName.vt = VT_LPWSTR; - THROW_IF_FAILED(propertyStore->SetValue(PKEY_AppUserModel_RelaunchDisplayNameResource, propVariantAppName)); -} -CATCH_LOG() - int main() { // Retrieve the app scenario. @@ -175,7 +148,8 @@ int main() const PACKAGE_VERSION minVersion{}; RETURN_IF_FAILED(MddBootstrapInitialize(c_Version_MajorMinor, nullptr, minVersion)); - SetDisplayNameAndIcon(); + // Not mandatory, but it's highly recommended to specify AppUserModelId + THROW_IF_FAILED(SetCurrentProcessExplicitAppUserModelID(L"TestAppId")); } std::wcout << L"--------------------------------\n"; diff --git a/test/TestApps/ToastNotificationsDemoApp/resource.h b/test/TestApps/ToastNotificationsDemoApp/resource.h new file mode 100644 index 0000000000..16f2e21364 --- /dev/null +++ b/test/TestApps/ToastNotificationsDemoApp/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by ToastNotificationsDemoApp.rc +// +#define IDI_ICON1 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif