1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim:set ts=2 sts=2 sw=2 et cin: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 #include "ToastNotificationHandler.h"
8
9 #include "WidgetUtils.h"
10 #include "WinTaskbar.h"
11 #include "WinUtils.h"
12 #include "imgIContainer.h"
13 #include "imgIRequest.h"
14 #include "mozilla/WindowsVersion.h"
15 #include "mozilla/gfx/2D.h"
16 #include "nsDirectoryServiceDefs.h"
17 #include "nsIStringBundle.h"
18 #include "nsIURI.h"
19 #include "nsIUUIDGenerator.h"
20 #include "nsIWidget.h"
21 #include "nsIWindowMediator.h"
22 #include "nsNetUtil.h"
23 #include "nsPIDOMWindow.h"
24 #include "nsProxyRelease.h"
25
26 #include "ToastNotification.h"
27
28 namespace mozilla {
29 namespace widget {
30
31 typedef ABI::Windows::Foundation::ITypedEventHandler<
32 ABI::Windows::UI::Notifications::ToastNotification*, IInspectable*>
33 ToastActivationHandler;
34 typedef ABI::Windows::Foundation::ITypedEventHandler<
35 ABI::Windows::UI::Notifications::ToastNotification*,
36 ABI::Windows::UI::Notifications::ToastDismissedEventArgs*>
37 ToastDismissedHandler;
38 typedef ABI::Windows::Foundation::ITypedEventHandler<
39 ABI::Windows::UI::Notifications::ToastNotification*,
40 ABI::Windows::UI::Notifications::ToastFailedEventArgs*>
41 ToastFailedHandler;
42
43 using namespace ABI::Windows::Data::Xml::Dom;
44 using namespace ABI::Windows::Foundation;
45 using namespace ABI::Windows::UI::Notifications;
46 using namespace Microsoft::WRL;
47 using namespace Microsoft::WRL::Wrappers;
48 using namespace mozilla;
49
NS_IMPL_ISUPPORTS(ToastNotificationHandler,nsIAlertNotificationImageListener)50 NS_IMPL_ISUPPORTS(ToastNotificationHandler, nsIAlertNotificationImageListener)
51
52 static bool SetNodeValueString(const nsString& aString, IXmlNode* node,
53 IXmlDocument* xml) {
54 ComPtr<IXmlText> inputText;
55 if (NS_WARN_IF(FAILED(xml->CreateTextNode(
56 HStringReference(static_cast<const wchar_t*>(aString.get())).Get(),
57 &inputText)))) {
58 return false;
59 }
60 ComPtr<IXmlNode> inputTextNode;
61 if (NS_WARN_IF(FAILED(inputText.As(&inputTextNode)))) {
62 return false;
63 }
64 ComPtr<IXmlNode> appendedChild;
65 if (NS_WARN_IF(
66 FAILED(node->AppendChild(inputTextNode.Get(), &appendedChild)))) {
67 return false;
68 }
69 return true;
70 }
71
SetAttribute(IXmlElement * element,const HSTRING name,const nsAString & value)72 static bool SetAttribute(IXmlElement* element, const HSTRING name,
73 const nsAString& value) {
74 HSTRING valueStr = HStringReference(static_cast<const wchar_t*>(
75 PromiseFlatString(value).get()))
76 .Get();
77 if (NS_WARN_IF(FAILED(element->SetAttribute(name, valueStr)))) {
78 return false;
79 }
80 return true;
81 }
82
AddActionNode(IXmlDocument * toastXml,IXmlNode * actionsNode,const nsAString & actionTitle,const nsAString & actionArgs)83 static bool AddActionNode(IXmlDocument* toastXml, IXmlNode* actionsNode,
84 const nsAString& actionTitle,
85 const nsAString& actionArgs) {
86 ComPtr<IXmlElement> action;
87 HRESULT hr =
88 toastXml->CreateElement(HStringReference(L"action").Get(), &action);
89 if (NS_WARN_IF(FAILED(hr))) {
90 return false;
91 }
92
93 if (NS_WARN_IF(!SetAttribute(action.Get(), HStringReference(L"content").Get(),
94 actionTitle))) {
95 return false;
96 }
97
98 if (NS_WARN_IF(!SetAttribute(
99 action.Get(), HStringReference(L"arguments").Get(), actionArgs))) {
100 return false;
101 }
102 if (NS_WARN_IF(!SetAttribute(action.Get(),
103 HStringReference(L"placement").Get(),
104 u"contextmenu"_ns))) {
105 return false;
106 }
107
108 // Add <action> to <actions>
109 ComPtr<IXmlNode> actionNode;
110 hr = action.As(&actionNode);
111 if (NS_WARN_IF(FAILED(hr))) {
112 return false;
113 }
114
115 ComPtr<IXmlNode> appendedChild;
116 hr = actionsNode->AppendChild(actionNode.Get(), &appendedChild);
117 if (NS_WARN_IF(FAILED(hr))) {
118 return false;
119 }
120
121 return true;
122 }
123
124 static ComPtr<IToastNotificationManagerStatics>
GetToastNotificationManagerStatics()125 GetToastNotificationManagerStatics() {
126 ComPtr<IToastNotificationManagerStatics> toastNotificationManagerStatics;
127 if (NS_WARN_IF(FAILED(GetActivationFactory(
128 HStringReference(
129 RuntimeClass_Windows_UI_Notifications_ToastNotificationManager)
130 .Get(),
131 &toastNotificationManagerStatics)))) {
132 return nullptr;
133 }
134
135 return toastNotificationManagerStatics;
136 }
137
~ToastNotificationHandler()138 ToastNotificationHandler::~ToastNotificationHandler() {
139 if (mImageRequest) {
140 mImageRequest->Cancel(NS_BINDING_ABORTED);
141 mImageRequest = nullptr;
142 }
143
144 if (mHasImage) {
145 DebugOnly<nsresult> rv = mImageFile->Remove(false);
146 NS_ASSERTION(NS_SUCCEEDED(rv), "Cannot remove temporary image file");
147 }
148
149 UnregisterHandler();
150 }
151
UnregisterHandler()152 void ToastNotificationHandler::UnregisterHandler() {
153 if (mNotification && mNotifier) {
154 mNotification->remove_Dismissed(mDismissedToken);
155 mNotification->remove_Activated(mActivatedToken);
156 mNotification->remove_Failed(mFailedToken);
157 mNotifier->Hide(mNotification.Get());
158 }
159
160 mNotification = nullptr;
161 mNotifier = nullptr;
162
163 SendFinished();
164 }
165
InitializeXmlForTemplate(ToastTemplateType templateType)166 ComPtr<IXmlDocument> ToastNotificationHandler::InitializeXmlForTemplate(
167 ToastTemplateType templateType) {
168 ComPtr<IToastNotificationManagerStatics> toastNotificationManagerStatics =
169 GetToastNotificationManagerStatics();
170
171 ComPtr<IXmlDocument> toastXml;
172 toastNotificationManagerStatics->GetTemplateContent(templateType, &toastXml);
173
174 return toastXml;
175 }
176
InitAlertAsync(nsIAlertNotification * aAlert)177 nsresult ToastNotificationHandler::InitAlertAsync(
178 nsIAlertNotification* aAlert) {
179 return aAlert->LoadImage(/* aTimeout = */ 0, this, /* aUserData = */ nullptr,
180 getter_AddRefs(mImageRequest));
181 }
182
ShowAlert()183 bool ToastNotificationHandler::ShowAlert() {
184 if (!mBackend->IsActiveHandler(mName, this)) {
185 return true;
186 }
187
188 ToastTemplateType toastTemplate;
189 if (mHostPort.IsEmpty()) {
190 toastTemplate =
191 mHasImage ? ToastTemplateType::ToastTemplateType_ToastImageAndText03
192 : ToastTemplateType::ToastTemplateType_ToastText03;
193 } else {
194 toastTemplate =
195 mHasImage ? ToastTemplateType::ToastTemplateType_ToastImageAndText04
196 : ToastTemplateType::ToastTemplateType_ToastText04;
197 }
198
199 ComPtr<IXmlDocument> toastXml = InitializeXmlForTemplate(toastTemplate);
200 if (!toastXml) {
201 return false;
202 }
203
204 HRESULT hr;
205
206 if (mHasImage) {
207 ComPtr<IXmlNodeList> toastImageElements;
208 hr = toastXml->GetElementsByTagName(HStringReference(L"image").Get(),
209 &toastImageElements);
210 if (NS_WARN_IF(FAILED(hr))) {
211 return false;
212 }
213 ComPtr<IXmlNode> imageNode;
214 hr = toastImageElements->Item(0, &imageNode);
215 if (NS_WARN_IF(FAILED(hr))) {
216 return false;
217 }
218 ComPtr<IXmlElement> image;
219 hr = imageNode.As(&image);
220 if (NS_WARN_IF(FAILED(hr))) {
221 return false;
222 }
223 if (NS_WARN_IF(!SetAttribute(image.Get(), HStringReference(L"src").Get(),
224 mImageUri))) {
225 return false;
226 }
227 }
228
229 ComPtr<IXmlNodeList> toastTextElements;
230 hr = toastXml->GetElementsByTagName(HStringReference(L"text").Get(),
231 &toastTextElements);
232 if (NS_WARN_IF(FAILED(hr))) {
233 return false;
234 }
235
236 ComPtr<IXmlNode> titleTextNodeRoot;
237 hr = toastTextElements->Item(0, &titleTextNodeRoot);
238 if (NS_WARN_IF(FAILED(hr))) {
239 return false;
240 }
241 ComPtr<IXmlNode> msgTextNodeRoot;
242 hr = toastTextElements->Item(1, &msgTextNodeRoot);
243 if (NS_WARN_IF(FAILED(hr))) {
244 return false;
245 }
246
247 if (NS_WARN_IF(!SetNodeValueString(mTitle, titleTextNodeRoot.Get(),
248 toastXml.Get()))) {
249 return false;
250 }
251 if (NS_WARN_IF(
252 !SetNodeValueString(mMsg, msgTextNodeRoot.Get(), toastXml.Get()))) {
253 return false;
254 }
255
256 ComPtr<IXmlNodeList> toastElements;
257 hr = toastXml->GetElementsByTagName(HStringReference(L"toast").Get(),
258 &toastElements);
259 if (NS_WARN_IF(FAILED(hr))) {
260 return false;
261 }
262
263 ComPtr<IXmlNode> toastNodeRoot;
264 hr = toastElements->Item(0, &toastNodeRoot);
265 if (NS_WARN_IF(FAILED(hr))) {
266 return false;
267 }
268
269 ComPtr<IXmlElement> actions;
270 hr = toastXml->CreateElement(HStringReference(L"actions").Get(), &actions);
271 if (NS_WARN_IF(FAILED(hr))) {
272 return false;
273 }
274
275 ComPtr<IXmlNode> actionsNode;
276 hr = actions.As(&actionsNode);
277 if (NS_WARN_IF(FAILED(hr))) {
278 return false;
279 }
280
281 nsCOMPtr<nsIStringBundleService> sbs =
282 do_GetService(NS_STRINGBUNDLE_CONTRACTID);
283 if (NS_WARN_IF(!sbs)) {
284 return false;
285 }
286
287 nsCOMPtr<nsIStringBundle> bundle;
288 sbs->CreateBundle("chrome://alerts/locale/alert.properties",
289 getter_AddRefs(bundle));
290 if (NS_WARN_IF(!bundle)) {
291 return false;
292 }
293
294 if (!mHostPort.IsEmpty()) {
295 AutoTArray<nsString, 1> formatStrings = {mHostPort};
296
297 ComPtr<IXmlNode> urlTextNodeRoot;
298 hr = toastTextElements->Item(2, &urlTextNodeRoot);
299 if (NS_WARN_IF(FAILED(hr))) {
300 return false;
301 }
302
303 nsAutoString urlReference;
304 bundle->FormatStringFromName("source.label", formatStrings, urlReference);
305
306 if (NS_WARN_IF(!SetNodeValueString(urlReference, urlTextNodeRoot.Get(),
307 toastXml.Get()))) {
308 return false;
309 }
310
311 if (IsWin10AnniversaryUpdateOrLater()) {
312 ComPtr<IXmlElement> placementText;
313 hr = urlTextNodeRoot.As(&placementText);
314 if (SUCCEEDED(hr)) {
315 // placement is supported on Windows 10 Anniversary Update or later
316 SetAttribute(placementText.Get(), HStringReference(L"placement").Get(),
317 u"attribution"_ns);
318 }
319 }
320
321 nsAutoString disableButtonTitle;
322 bundle->FormatStringFromName("webActions.disableForOrigin.label",
323 formatStrings, disableButtonTitle);
324
325 AddActionNode(toastXml.Get(), actionsNode.Get(), disableButtonTitle,
326 u"snooze"_ns);
327 }
328
329 nsAutoString settingsButtonTitle;
330 bundle->GetStringFromName("webActions.settings.label", settingsButtonTitle);
331 AddActionNode(toastXml.Get(), actionsNode.Get(), settingsButtonTitle,
332 u"settings"_ns);
333
334 ComPtr<IXmlNode> appendedChild;
335 hr = toastNodeRoot->AppendChild(actionsNode.Get(), &appendedChild);
336 if (NS_WARN_IF(FAILED(hr))) {
337 return false;
338 }
339
340 return CreateWindowsNotificationFromXml(toastXml.Get());
341 }
342
CreateWindowsNotificationFromXml(IXmlDocument * aXml)343 bool ToastNotificationHandler::CreateWindowsNotificationFromXml(
344 IXmlDocument* aXml) {
345 ComPtr<IToastNotificationFactory> factory;
346 HRESULT hr = GetActivationFactory(
347 HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotification)
348 .Get(),
349 &factory);
350 if (NS_WARN_IF(FAILED(hr))) {
351 return false;
352 }
353
354 hr = factory->CreateToastNotification(aXml, &mNotification);
355 if (NS_WARN_IF(FAILED(hr))) {
356 return false;
357 }
358
359 RefPtr<ToastNotificationHandler> self = this;
360
361 hr = mNotification->add_Activated(
362 Callback<ToastActivationHandler>([self](IToastNotification* aNotification,
363 IInspectable* aInspectable) {
364 return self->OnActivate(aNotification, aInspectable);
365 }).Get(),
366 &mActivatedToken);
367 if (NS_WARN_IF(FAILED(hr))) {
368 return false;
369 }
370
371 hr = mNotification->add_Dismissed(
372 Callback<ToastDismissedHandler>([self](IToastNotification* aNotification,
373 IToastDismissedEventArgs* aArgs) {
374 return self->OnDismiss(aNotification, aArgs);
375 }).Get(),
376 &mDismissedToken);
377 if (NS_WARN_IF(FAILED(hr))) {
378 return false;
379 }
380
381 hr = mNotification->add_Failed(
382 Callback<ToastFailedHandler>([self](IToastNotification* aNotification,
383 IToastFailedEventArgs* aArgs) {
384 return self->OnFail(aNotification, aArgs);
385 }).Get(),
386 &mFailedToken);
387 if (NS_WARN_IF(FAILED(hr))) {
388 return false;
389 }
390
391 ComPtr<IToastNotificationManagerStatics> toastNotificationManagerStatics =
392 GetToastNotificationManagerStatics();
393 if (NS_WARN_IF(!toastNotificationManagerStatics)) {
394 return false;
395 }
396
397 nsAutoString uid;
398 if (NS_WARN_IF(!WinTaskbar::GetAppUserModelID(uid))) {
399 return false;
400 }
401
402 HSTRING uidStr =
403 HStringReference(static_cast<const wchar_t*>(uid.get())).Get();
404 hr = toastNotificationManagerStatics->CreateToastNotifierWithId(uidStr,
405 &mNotifier);
406 if (NS_WARN_IF(FAILED(hr))) {
407 return false;
408 }
409
410 hr = mNotifier->Show(mNotification.Get());
411 if (NS_WARN_IF(FAILED(hr))) {
412 return false;
413 }
414
415 if (mAlertListener) {
416 mAlertListener->Observe(nullptr, "alertshow", mCookie.get());
417 }
418
419 return true;
420 }
421
SendFinished()422 void ToastNotificationHandler::SendFinished() {
423 if (!mSentFinished && mAlertListener) {
424 mAlertListener->Observe(nullptr, "alertfinished", mCookie.get());
425 }
426
427 mSentFinished = true;
428 }
429
430 HRESULT
OnActivate(IToastNotification * notification,IInspectable * inspectable)431 ToastNotificationHandler::OnActivate(IToastNotification* notification,
432 IInspectable* inspectable) {
433 if (mAlertListener) {
434 nsAutoString argString;
435 if (inspectable) {
436 ComPtr<IToastActivatedEventArgs> eventArgs;
437 HRESULT hr = inspectable->QueryInterface(
438 __uuidof(IToastActivatedEventArgs), (void**)&eventArgs);
439 if (SUCCEEDED(hr)) {
440 HSTRING arguments;
441 hr = eventArgs->get_Arguments(&arguments);
442 if (SUCCEEDED(hr)) {
443 uint32_t len = 0;
444 const wchar_t* buffer = WindowsGetStringRawBuffer(arguments, &len);
445 if (buffer) {
446 argString.Assign(buffer, len);
447 }
448 }
449 }
450 }
451
452 if (argString.EqualsLiteral("settings")) {
453 mAlertListener->Observe(nullptr, "alertsettingscallback", mCookie.get());
454 } else if (argString.EqualsLiteral("snooze")) {
455 mAlertListener->Observe(nullptr, "alertdisablecallback", mCookie.get());
456 } else if (mClickable) {
457 // When clicking toast, focus moves to another process, but we want to set
458 // focus on Firefox process.
459 nsCOMPtr<nsIWindowMediator> winMediator(
460 do_GetService(NS_WINDOWMEDIATOR_CONTRACTID));
461 if (winMediator) {
462 nsCOMPtr<mozIDOMWindowProxy> navWin;
463 winMediator->GetMostRecentWindow(u"navigator:browser",
464 getter_AddRefs(navWin));
465 if (navWin) {
466 nsCOMPtr<nsIWidget> widget =
467 WidgetUtils::DOMWindowToWidget(nsPIDOMWindowOuter::From(navWin));
468 if (widget) {
469 SetForegroundWindow(
470 static_cast<HWND>(widget->GetNativeData(NS_NATIVE_WINDOW)));
471 }
472 }
473 }
474 mAlertListener->Observe(nullptr, "alertclickcallback", mCookie.get());
475 }
476 }
477 mBackend->RemoveHandler(mName, this);
478 return S_OK;
479 }
480
481 HRESULT
OnDismiss(IToastNotification * notification,IToastDismissedEventArgs * aArgs)482 ToastNotificationHandler::OnDismiss(IToastNotification* notification,
483 IToastDismissedEventArgs* aArgs) {
484 SendFinished();
485 mBackend->RemoveHandler(mName, this);
486 return S_OK;
487 }
488
489 HRESULT
OnFail(IToastNotification * notification,IToastFailedEventArgs * aArgs)490 ToastNotificationHandler::OnFail(IToastNotification* notification,
491 IToastFailedEventArgs* aArgs) {
492 SendFinished();
493 mBackend->RemoveHandler(mName, this);
494 return S_OK;
495 }
496
TryShowAlert()497 nsresult ToastNotificationHandler::TryShowAlert() {
498 if (NS_WARN_IF(!ShowAlert())) {
499 mBackend->RemoveHandler(mName, this);
500 return NS_ERROR_FAILURE;
501 }
502 return NS_OK;
503 }
504
505 NS_IMETHODIMP
OnImageMissing(nsISupports *)506 ToastNotificationHandler::OnImageMissing(nsISupports*) {
507 return TryShowAlert();
508 }
509
510 NS_IMETHODIMP
OnImageReady(nsISupports *,imgIRequest * aRequest)511 ToastNotificationHandler::OnImageReady(nsISupports*, imgIRequest* aRequest) {
512 nsresult rv = AsyncSaveImage(aRequest);
513 if (NS_FAILED(rv)) {
514 return TryShowAlert();
515 }
516 return rv;
517 }
518
AsyncSaveImage(imgIRequest * aRequest)519 nsresult ToastNotificationHandler::AsyncSaveImage(imgIRequest* aRequest) {
520 nsresult rv =
521 NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(mImageFile));
522 if (NS_WARN_IF(NS_FAILED(rv))) {
523 return rv;
524 }
525
526 rv = mImageFile->Append(u"notificationimages"_ns);
527 if (NS_WARN_IF(NS_FAILED(rv))) {
528 return rv;
529 }
530
531 rv = mImageFile->Create(nsIFile::DIRECTORY_TYPE, 0500);
532 if (NS_FAILED(rv) && rv != NS_ERROR_FILE_ALREADY_EXISTS) {
533 return rv;
534 }
535
536 nsCOMPtr<nsIUUIDGenerator> idGen =
537 do_GetService("@mozilla.org/uuid-generator;1", &rv);
538 if (NS_WARN_IF(NS_FAILED(rv))) {
539 return rv;
540 }
541
542 nsID uuid;
543 rv = idGen->GenerateUUIDInPlace(&uuid);
544 if (NS_WARN_IF(NS_FAILED(rv))) {
545 return rv;
546 }
547
548 char uuidChars[NSID_LENGTH];
549 uuid.ToProvidedString(uuidChars);
550 // Remove the brackets at the beginning and ending of the generated UUID.
551 nsAutoCString uuidStr(Substring(uuidChars + 1, uuidChars + NSID_LENGTH - 2));
552 uuidStr.AppendLiteral(".bmp");
553 mImageFile->AppendNative(uuidStr);
554
555 nsCOMPtr<imgIContainer> imgContainer;
556 rv = aRequest->GetImage(getter_AddRefs(imgContainer));
557 if (NS_WARN_IF(NS_FAILED(rv))) {
558 return rv;
559 }
560
561 nsMainThreadPtrHandle<ToastNotificationHandler> self(
562 new nsMainThreadPtrHolder<ToastNotificationHandler>(
563 "ToastNotificationHandler", this));
564
565 nsCOMPtr<nsIFile> imageFile(mImageFile);
566 RefPtr<mozilla::gfx::SourceSurface> surface = imgContainer->GetFrame(
567 imgIContainer::FRAME_FIRST,
568 imgIContainer::FLAG_SYNC_DECODE | imgIContainer::FLAG_ASYNC_NOTIFY);
569 nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
570 "ToastNotificationHandler::AsyncWriteBitmap",
571 [self, imageFile, surface]() -> void {
572 nsresult rv;
573 if (!surface) {
574 rv = NS_ERROR_FAILURE;
575 } else {
576 rv = WinUtils::WriteBitmap(imageFile, surface);
577 }
578
579 nsCOMPtr<nsIRunnable> cbRunnable = NS_NewRunnableFunction(
580 "ToastNotificationHandler::AsyncWriteBitmapCb",
581 [self, rv]() -> void {
582 auto handler = const_cast<ToastNotificationHandler*>(self.get());
583 handler->OnWriteBitmapFinished(rv);
584 });
585
586 NS_DispatchToMainThread(cbRunnable);
587 });
588
589 return mBackend->BackgroundDispatch(r);
590 }
591
OnWriteBitmapFinished(nsresult rv)592 void ToastNotificationHandler::OnWriteBitmapFinished(nsresult rv) {
593 if (NS_SUCCEEDED(rv)) {
594 OnWriteBitmapSuccess();
595 }
596 TryShowAlert();
597 }
598
OnWriteBitmapSuccess()599 nsresult ToastNotificationHandler::OnWriteBitmapSuccess() {
600 nsresult rv;
601
602 nsCOMPtr<nsIURI> fileURI;
603 rv = NS_NewFileURI(getter_AddRefs(fileURI), mImageFile);
604 if (NS_WARN_IF(NS_FAILED(rv))) {
605 return rv;
606 }
607
608 nsAutoCString uriStr;
609 rv = fileURI->GetSpec(uriStr);
610 if (NS_WARN_IF(NS_FAILED(rv))) {
611 return rv;
612 }
613
614 AppendUTF8toUTF16(uriStr, mImageUri);
615
616 mHasImage = true;
617
618 return NS_OK;
619 }
620
621 } // namespace widget
622 } // namespace mozilla
623