/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim:set ts=2 sts=2 sw=2 et cin: */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #include "ToastNotificationHandler.h" #include "WidgetUtils.h" #include "WinTaskbar.h" #include "WinUtils.h" #include "imgIContainer.h" #include "imgIRequest.h" #include "mozilla/WindowsVersion.h" #include "mozilla/gfx/2D.h" #include "nsDirectoryServiceDefs.h" #include "nsIStringBundle.h" #include "nsIURI.h" #include "nsIUUIDGenerator.h" #include "nsIWidget.h" #include "nsIWindowMediator.h" #include "nsNetUtil.h" #include "nsPIDOMWindow.h" #include "nsProxyRelease.h" #include "ToastNotification.h" namespace mozilla { namespace widget { typedef ABI::Windows::Foundation::ITypedEventHandler< ABI::Windows::UI::Notifications::ToastNotification*, IInspectable*> ToastActivationHandler; typedef ABI::Windows::Foundation::ITypedEventHandler< ABI::Windows::UI::Notifications::ToastNotification*, ABI::Windows::UI::Notifications::ToastDismissedEventArgs*> ToastDismissedHandler; typedef ABI::Windows::Foundation::ITypedEventHandler< ABI::Windows::UI::Notifications::ToastNotification*, ABI::Windows::UI::Notifications::ToastFailedEventArgs*> ToastFailedHandler; using namespace ABI::Windows::Data::Xml::Dom; using namespace ABI::Windows::Foundation; using namespace ABI::Windows::UI::Notifications; using namespace Microsoft::WRL; using namespace Microsoft::WRL::Wrappers; using namespace mozilla; NS_IMPL_ISUPPORTS(ToastNotificationHandler, nsIAlertNotificationImageListener) static bool SetNodeValueString(const nsString& aString, IXmlNode* node, IXmlDocument* xml) { ComPtr inputText; if (NS_WARN_IF(FAILED(xml->CreateTextNode( HStringReference(static_cast(aString.get())).Get(), &inputText)))) { return false; } ComPtr inputTextNode; if (NS_WARN_IF(FAILED(inputText.As(&inputTextNode)))) { return false; } ComPtr appendedChild; if (NS_WARN_IF( FAILED(node->AppendChild(inputTextNode.Get(), &appendedChild)))) { return false; } return true; } static bool SetAttribute(IXmlElement* element, const HSTRING name, const nsAString& value) { HSTRING valueStr = HStringReference(static_cast( PromiseFlatString(value).get())) .Get(); if (NS_WARN_IF(FAILED(element->SetAttribute(name, valueStr)))) { return false; } return true; } static bool AddActionNode(IXmlDocument* toastXml, IXmlNode* actionsNode, const nsAString& actionTitle, const nsAString& actionArgs) { ComPtr action; HRESULT hr = toastXml->CreateElement(HStringReference(L"action").Get(), &action); if (NS_WARN_IF(FAILED(hr))) { return false; } if (NS_WARN_IF(!SetAttribute(action.Get(), HStringReference(L"content").Get(), actionTitle))) { return false; } if (NS_WARN_IF(!SetAttribute( action.Get(), HStringReference(L"arguments").Get(), actionArgs))) { return false; } if (NS_WARN_IF(!SetAttribute(action.Get(), HStringReference(L"placement").Get(), u"contextmenu"_ns))) { return false; } // Add to ComPtr actionNode; hr = action.As(&actionNode); if (NS_WARN_IF(FAILED(hr))) { return false; } ComPtr appendedChild; hr = actionsNode->AppendChild(actionNode.Get(), &appendedChild); if (NS_WARN_IF(FAILED(hr))) { return false; } return true; } static ComPtr GetToastNotificationManagerStatics() { ComPtr toastNotificationManagerStatics; if (NS_WARN_IF(FAILED(GetActivationFactory( HStringReference( RuntimeClass_Windows_UI_Notifications_ToastNotificationManager) .Get(), &toastNotificationManagerStatics)))) { return nullptr; } return toastNotificationManagerStatics; } ToastNotificationHandler::~ToastNotificationHandler() { if (mImageRequest) { mImageRequest->Cancel(NS_BINDING_ABORTED); mImageRequest = nullptr; } if (mHasImage) { DebugOnly rv = mImageFile->Remove(false); NS_ASSERTION(NS_SUCCEEDED(rv), "Cannot remove temporary image file"); } UnregisterHandler(); } void ToastNotificationHandler::UnregisterHandler() { if (mNotification && mNotifier) { mNotification->remove_Dismissed(mDismissedToken); mNotification->remove_Activated(mActivatedToken); mNotification->remove_Failed(mFailedToken); mNotifier->Hide(mNotification.Get()); } mNotification = nullptr; mNotifier = nullptr; SendFinished(); } ComPtr ToastNotificationHandler::InitializeXmlForTemplate( ToastTemplateType templateType) { ComPtr toastNotificationManagerStatics = GetToastNotificationManagerStatics(); ComPtr toastXml; toastNotificationManagerStatics->GetTemplateContent(templateType, &toastXml); return toastXml; } nsresult ToastNotificationHandler::InitAlertAsync( nsIAlertNotification* aAlert) { return aAlert->LoadImage(/* aTimeout = */ 0, this, /* aUserData = */ nullptr, getter_AddRefs(mImageRequest)); } bool ToastNotificationHandler::ShowAlert() { if (!mBackend->IsActiveHandler(mName, this)) { return true; } ToastTemplateType toastTemplate; if (mHostPort.IsEmpty()) { toastTemplate = mHasImage ? ToastTemplateType::ToastTemplateType_ToastImageAndText03 : ToastTemplateType::ToastTemplateType_ToastText03; } else { toastTemplate = mHasImage ? ToastTemplateType::ToastTemplateType_ToastImageAndText04 : ToastTemplateType::ToastTemplateType_ToastText04; } ComPtr toastXml = InitializeXmlForTemplate(toastTemplate); if (!toastXml) { return false; } HRESULT hr; if (mHasImage) { ComPtr toastImageElements; hr = toastXml->GetElementsByTagName(HStringReference(L"image").Get(), &toastImageElements); if (NS_WARN_IF(FAILED(hr))) { return false; } ComPtr imageNode; hr = toastImageElements->Item(0, &imageNode); if (NS_WARN_IF(FAILED(hr))) { return false; } ComPtr image; hr = imageNode.As(&image); if (NS_WARN_IF(FAILED(hr))) { return false; } if (NS_WARN_IF(!SetAttribute(image.Get(), HStringReference(L"src").Get(), mImageUri))) { return false; } } ComPtr toastTextElements; hr = toastXml->GetElementsByTagName(HStringReference(L"text").Get(), &toastTextElements); if (NS_WARN_IF(FAILED(hr))) { return false; } ComPtr titleTextNodeRoot; hr = toastTextElements->Item(0, &titleTextNodeRoot); if (NS_WARN_IF(FAILED(hr))) { return false; } ComPtr msgTextNodeRoot; hr = toastTextElements->Item(1, &msgTextNodeRoot); if (NS_WARN_IF(FAILED(hr))) { return false; } if (NS_WARN_IF(!SetNodeValueString(mTitle, titleTextNodeRoot.Get(), toastXml.Get()))) { return false; } if (NS_WARN_IF( !SetNodeValueString(mMsg, msgTextNodeRoot.Get(), toastXml.Get()))) { return false; } ComPtr toastElements; hr = toastXml->GetElementsByTagName(HStringReference(L"toast").Get(), &toastElements); if (NS_WARN_IF(FAILED(hr))) { return false; } ComPtr toastNodeRoot; hr = toastElements->Item(0, &toastNodeRoot); if (NS_WARN_IF(FAILED(hr))) { return false; } ComPtr actions; hr = toastXml->CreateElement(HStringReference(L"actions").Get(), &actions); if (NS_WARN_IF(FAILED(hr))) { return false; } ComPtr actionsNode; hr = actions.As(&actionsNode); if (NS_WARN_IF(FAILED(hr))) { return false; } nsCOMPtr sbs = do_GetService(NS_STRINGBUNDLE_CONTRACTID); if (NS_WARN_IF(!sbs)) { return false; } nsCOMPtr bundle; sbs->CreateBundle("chrome://alerts/locale/alert.properties", getter_AddRefs(bundle)); if (NS_WARN_IF(!bundle)) { return false; } if (!mHostPort.IsEmpty()) { AutoTArray formatStrings = {mHostPort}; ComPtr urlTextNodeRoot; hr = toastTextElements->Item(2, &urlTextNodeRoot); if (NS_WARN_IF(FAILED(hr))) { return false; } nsAutoString urlReference; bundle->FormatStringFromName("source.label", formatStrings, urlReference); if (NS_WARN_IF(!SetNodeValueString(urlReference, urlTextNodeRoot.Get(), toastXml.Get()))) { return false; } if (IsWin10AnniversaryUpdateOrLater()) { ComPtr placementText; hr = urlTextNodeRoot.As(&placementText); if (SUCCEEDED(hr)) { // placement is supported on Windows 10 Anniversary Update or later SetAttribute(placementText.Get(), HStringReference(L"placement").Get(), u"attribution"_ns); } } nsAutoString disableButtonTitle; bundle->FormatStringFromName("webActions.disableForOrigin.label", formatStrings, disableButtonTitle); AddActionNode(toastXml.Get(), actionsNode.Get(), disableButtonTitle, u"snooze"_ns); } nsAutoString settingsButtonTitle; bundle->GetStringFromName("webActions.settings.label", settingsButtonTitle); AddActionNode(toastXml.Get(), actionsNode.Get(), settingsButtonTitle, u"settings"_ns); ComPtr appendedChild; hr = toastNodeRoot->AppendChild(actionsNode.Get(), &appendedChild); if (NS_WARN_IF(FAILED(hr))) { return false; } return CreateWindowsNotificationFromXml(toastXml.Get()); } bool ToastNotificationHandler::CreateWindowsNotificationFromXml( IXmlDocument* aXml) { ComPtr factory; HRESULT hr = GetActivationFactory( HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotification) .Get(), &factory); if (NS_WARN_IF(FAILED(hr))) { return false; } hr = factory->CreateToastNotification(aXml, &mNotification); if (NS_WARN_IF(FAILED(hr))) { return false; } RefPtr self = this; hr = mNotification->add_Activated( Callback([self](IToastNotification* aNotification, IInspectable* aInspectable) { return self->OnActivate(aNotification, aInspectable); }).Get(), &mActivatedToken); if (NS_WARN_IF(FAILED(hr))) { return false; } hr = mNotification->add_Dismissed( Callback([self](IToastNotification* aNotification, IToastDismissedEventArgs* aArgs) { return self->OnDismiss(aNotification, aArgs); }).Get(), &mDismissedToken); if (NS_WARN_IF(FAILED(hr))) { return false; } hr = mNotification->add_Failed( Callback([self](IToastNotification* aNotification, IToastFailedEventArgs* aArgs) { return self->OnFail(aNotification, aArgs); }).Get(), &mFailedToken); if (NS_WARN_IF(FAILED(hr))) { return false; } ComPtr toastNotificationManagerStatics = GetToastNotificationManagerStatics(); if (NS_WARN_IF(!toastNotificationManagerStatics)) { return false; } nsAutoString uid; if (NS_WARN_IF(!WinTaskbar::GetAppUserModelID(uid))) { return false; } HSTRING uidStr = HStringReference(static_cast(uid.get())).Get(); hr = toastNotificationManagerStatics->CreateToastNotifierWithId(uidStr, &mNotifier); if (NS_WARN_IF(FAILED(hr))) { return false; } hr = mNotifier->Show(mNotification.Get()); if (NS_WARN_IF(FAILED(hr))) { return false; } if (mAlertListener) { mAlertListener->Observe(nullptr, "alertshow", mCookie.get()); } return true; } void ToastNotificationHandler::SendFinished() { if (!mSentFinished && mAlertListener) { mAlertListener->Observe(nullptr, "alertfinished", mCookie.get()); } mSentFinished = true; } HRESULT ToastNotificationHandler::OnActivate(IToastNotification* notification, IInspectable* inspectable) { if (mAlertListener) { nsAutoString argString; if (inspectable) { ComPtr eventArgs; HRESULT hr = inspectable->QueryInterface( __uuidof(IToastActivatedEventArgs), (void**)&eventArgs); if (SUCCEEDED(hr)) { HSTRING arguments; hr = eventArgs->get_Arguments(&arguments); if (SUCCEEDED(hr)) { uint32_t len = 0; const wchar_t* buffer = WindowsGetStringRawBuffer(arguments, &len); if (buffer) { argString.Assign(buffer, len); } } } } if (argString.EqualsLiteral("settings")) { mAlertListener->Observe(nullptr, "alertsettingscallback", mCookie.get()); } else if (argString.EqualsLiteral("snooze")) { mAlertListener->Observe(nullptr, "alertdisablecallback", mCookie.get()); } else if (mClickable) { // When clicking toast, focus moves to another process, but we want to set // focus on Firefox process. nsCOMPtr winMediator( do_GetService(NS_WINDOWMEDIATOR_CONTRACTID)); if (winMediator) { nsCOMPtr navWin; winMediator->GetMostRecentWindow(u"navigator:browser", getter_AddRefs(navWin)); if (navWin) { nsCOMPtr widget = WidgetUtils::DOMWindowToWidget(nsPIDOMWindowOuter::From(navWin)); if (widget) { SetForegroundWindow( static_cast(widget->GetNativeData(NS_NATIVE_WINDOW))); } } } mAlertListener->Observe(nullptr, "alertclickcallback", mCookie.get()); } } mBackend->RemoveHandler(mName, this); return S_OK; } HRESULT ToastNotificationHandler::OnDismiss(IToastNotification* notification, IToastDismissedEventArgs* aArgs) { SendFinished(); mBackend->RemoveHandler(mName, this); return S_OK; } HRESULT ToastNotificationHandler::OnFail(IToastNotification* notification, IToastFailedEventArgs* aArgs) { SendFinished(); mBackend->RemoveHandler(mName, this); return S_OK; } nsresult ToastNotificationHandler::TryShowAlert() { if (NS_WARN_IF(!ShowAlert())) { mBackend->RemoveHandler(mName, this); return NS_ERROR_FAILURE; } return NS_OK; } NS_IMETHODIMP ToastNotificationHandler::OnImageMissing(nsISupports*) { return TryShowAlert(); } NS_IMETHODIMP ToastNotificationHandler::OnImageReady(nsISupports*, imgIRequest* aRequest) { nsresult rv = AsyncSaveImage(aRequest); if (NS_FAILED(rv)) { return TryShowAlert(); } return rv; } nsresult ToastNotificationHandler::AsyncSaveImage(imgIRequest* aRequest) { nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(mImageFile)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = mImageFile->Append(u"notificationimages"_ns); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } rv = mImageFile->Create(nsIFile::DIRECTORY_TYPE, 0500); if (NS_FAILED(rv) && rv != NS_ERROR_FILE_ALREADY_EXISTS) { return rv; } nsCOMPtr idGen = do_GetService("@mozilla.org/uuid-generator;1", &rv); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsID uuid; rv = idGen->GenerateUUIDInPlace(&uuid); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } char uuidChars[NSID_LENGTH]; uuid.ToProvidedString(uuidChars); // Remove the brackets at the beginning and ending of the generated UUID. nsAutoCString uuidStr(Substring(uuidChars + 1, uuidChars + NSID_LENGTH - 2)); uuidStr.AppendLiteral(".bmp"); mImageFile->AppendNative(uuidStr); nsCOMPtr imgContainer; rv = aRequest->GetImage(getter_AddRefs(imgContainer)); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsMainThreadPtrHandle self( new nsMainThreadPtrHolder( "ToastNotificationHandler", this)); nsCOMPtr imageFile(mImageFile); RefPtr surface = imgContainer->GetFrame( imgIContainer::FRAME_FIRST, imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY); nsCOMPtr r = NS_NewRunnableFunction( "ToastNotificationHandler::AsyncWriteBitmap", [self, imageFile, surface]() -> void { nsresult rv; if (!surface) { rv = NS_ERROR_FAILURE; } else { rv = WinUtils::WriteBitmap(imageFile, surface); } nsCOMPtr cbRunnable = NS_NewRunnableFunction( "ToastNotificationHandler::AsyncWriteBitmapCb", [self, rv]() -> void { auto handler = const_cast(self.get()); handler->OnWriteBitmapFinished(rv); }); NS_DispatchToMainThread(cbRunnable); }); return mBackend->BackgroundDispatch(r); } void ToastNotificationHandler::OnWriteBitmapFinished(nsresult rv) { if (NS_SUCCEEDED(rv)) { OnWriteBitmapSuccess(); } TryShowAlert(); } nsresult ToastNotificationHandler::OnWriteBitmapSuccess() { nsresult rv; nsCOMPtr fileURI; rv = NS_NewFileURI(getter_AddRefs(fileURI), mImageFile); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsAutoCString uriStr; rv = fileURI->GetSpec(uriStr); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } AppendUTF8toUTF16(uriStr, mImageUri); mHasImage = true; return NS_OK; } } // namespace widget } // namespace mozilla