Bug 783232 - Hook nsIAlertService up to native toast notifications and use them for download complete alerts. r=jimm
authorMarina Samuel <msamuel@mozilla.com>
Wed, 07 Aug 2013 16:57:06 -0400
changeset 154581 7b37baf1e8b812f6dd4d0d376f745773b4c7517e
parent 154580 13d42e9b36e274e04385eae2e899ec8d070952b9
child 154582 b43afcd4e3476c30dedd350686cc3b2a714a8ecb
push id2961
push userlsblakk@mozilla.com
push dateMon, 28 Oct 2013 21:59:28 +0000
treeherdermozilla-beta@73ef4f13486f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjimm
bugs783232
milestone26.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 783232 - Hook nsIAlertService up to native toast notifications and use them for download complete alerts. r=jimm
browser/metro/base/content/browser.xul
browser/metro/base/content/downloads.js
browser/metro/base/content/helperui/AlertsHelper.js
browser/metro/components/AlertsService.js
browser/metro/locales/en-US/chrome/browser.properties
browser/metro/theme/browser.css
widget/nsIWinMetroUtils.idl
widget/windows/winrt/ToastNotificationHandler.cpp
widget/windows/winrt/ToastNotificationHandler.h
widget/windows/winrt/moz.build
widget/windows/winrt/nsWinMetroUtils.cpp
--- a/browser/metro/base/content/browser.xul
+++ b/browser/metro/base/content/browser.xul
@@ -853,20 +853,11 @@
       </vbox>
     </box>
 
     <box id="autofill-container" class="menu-container" hidden="true">
       <vbox id="autofill-popup" class="menu-popup">
         <richlistbox id="menupopup-commands" onclick="if (event.target != this) AutofillMenuUI.selectByIndex(this.selectedIndex);" flex="1"/>
       </vbox>
     </box>
-
-    <!-- alerts for content -->
-    <hbox id="alerts-container" hidden="true" align="start" bottom="0" onclick="AlertsHelper.click(event);">
-      <image id="alerts-image"/>
-      <vbox flex="1">
-        <label id="alerts-title" value=""/>
-        <description id="alerts-text" flex="1"/>
-      </vbox>
-    </hbox>
   </stack>
 
 </window>
--- a/browser/metro/base/content/downloads.js
+++ b/browser/metro/base/content/downloads.js
@@ -1,14 +1,15 @@
 // -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
 /* 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/. */
 
 const URI_GENERIC_ICON_DOWNLOAD = "chrome://browser/skin/images/alert-downloads-30.png";
+const TOAST_URI_GENERIC_ICON_DOWNLOAD = "ms-appx:///metro/chrome/chrome/skin/images/alert-downloads-30.png"
 
 var Downloads = {
   /**
    * _downloadCount keeps track of the number of downloads that a single
    * notification bar groups together. A download is grouped with other
    * downloads if it starts before other downloads have completed.
    */
   _downloadCount: 0,
@@ -149,36 +150,27 @@ var Downloads = {
   showPage: function dh_showPage(aDownload) {
     let id = aDownload.getAttribute("downloadId");
     let download = this.manager.getDownload(id);
     let uri = this._getReferrerOrSource(download);
     if (uri)
       BrowserUI.newTab(uri, Browser.selectedTab);
   },
 
-  showAlert: function dh_showAlert(aName, aMessage, aTitle, aIcon) {
+  showAlert: function dh_showAlert(aName, aMessage, aTitle, aIcon, aObserver) {
     var notifier = Cc["@mozilla.org/alerts-service;1"]
                      .getService(Ci.nsIAlertsService);
 
-    // Callback for tapping on the alert popup
-    let observer = {
-      observe: function (aSubject, aTopic, aData) {
-        if (aTopic == "alertclickcallback") {
-          // TODO: Bug 783232 turns this alert into a native toast. 
-        }
-      }
-    };
-
     if (!aTitle)
       aTitle = Strings.browser.GetStringFromName("alertDownloads");
 
     if (!aIcon)
-      aIcon = URI_GENERIC_ICON_DOWNLOAD;
+      aIcon = TOAST_URI_GENERIC_ICON_DOWNLOAD;
 
-    notifier.showAlertNotification(aIcon, aTitle, aMessage, true, "", observer, aName);
+    notifier.showAlertNotification(aIcon, aTitle, aMessage, true, "", aObserver, aName);
   },
 
   showNotification: function dh_showNotification(title, msg, buttons, priority) {
     return this._notificationBox.appendNotification(msg,
                                               title,
                                               URI_GENERIC_ICON_DOWNLOAD,
                                               priority,
                                               buttons);
@@ -252,16 +244,63 @@ var Downloads = {
           Downloads._downloadProgressIndicator.reset();
         }
       });
     }
     this.showNotification("download-complete", message, buttons,
       this._notificationBox.PRIORITY_WARNING_MEDIUM);
   },
 
+  _showDownloadCompleteToast: function (aDownload) {
+    let name = "DownloadComplete";
+    let msg = "";
+    let title = "";
+    let observer = null;
+    if (this._downloadCount > 1) {
+      title = PluralForm.get(this._downloadCount,
+                             Strings.browser.GetStringFromName("alertMultipleDownloadsComplete"))
+                             .replace("#1", this._downloadCount)
+      msg = PluralForm.get(2, Strings.browser.GetStringFromName("downloadShowInFiles"));
+
+      observer = {
+        observe: function (aSubject, aTopic, aData) {
+          switch (aTopic) {
+            case "alertclickcallback":
+              let fileURI = aDownload.target;
+              let file = Downloads._getLocalFile(fileURI);
+              file.reveal();
+
+              let downloadCompleteNotification =
+                Downloads._notificationBox.getNotificationWithValue("download-complete");
+              Downloads._notificationBox.removeNotification(downloadCompleteNotification);
+              break;
+          }
+        }
+      }
+    } else {
+      title = Strings.browser.formatStringFromName("alertDownloadsDone",
+        [aDownload.displayName], 1);
+      msg = Strings.browser.GetStringFromName("downloadRunNow");
+      observer = {
+        observe: function (aSubject, aTopic, aData) {
+          switch (aTopic) {
+            case "alertclickcallback":
+              Downloads.openDownload(aDownload);
+
+              let downloadCompleteNotification =
+                Downloads._notificationBox.getNotificationWithValue("download-complete");
+              Downloads._notificationBox.removeNotification(downloadCompleteNotification);
+              break;
+          }
+        }
+      }
+    }
+    this.showAlert(name, msg, title, null, observer);
+  },
+
   _updateCircularProgressMeter: function dv_updateCircularProgressMeter() {
     if (!this._progressNotificationInfo) {
       return;
     }
 
     let totPercent = 0;
     for (let info of this._progressNotificationInfo) {
       // info[0]          => download guid
@@ -386,16 +425,17 @@ var Downloads = {
         let runAfterDownload = this._runDownloadBooleanMap.get(download.targetFile.path);
         if (runAfterDownload) {
           this.openDownload(download);
         }
 
         this._runDownloadBooleanMap.delete(download.targetFile.path);
         if (this._downloadsInProgress == 0) {
           if (this._downloadCount > 1 || !runAfterDownload) {
+            this._showDownloadCompleteToast(download);
             this._showDownloadCompleteNotification(download);
           }
           this._progressNotificationInfo.clear();
           this._downloadCount = 0;
           this._notificationBox.removeNotification(this._progressNotification);
           this._progressNotification = null;
         }
         break;
--- a/browser/metro/base/content/helperui/AlertsHelper.js
+++ b/browser/metro/base/content/helperui/AlertsHelper.js
@@ -1,82 +1,25 @@
 /* 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/. */
 
 var AlertsHelper = {
-  _timeoutID: -1,
   _listener: null,
   _cookie: "",
-  _clickable: false,
-  get container() {
-    delete this.container;
-    let container = document.getElementById("alerts-container");
-
-    let self = this;
-    container.addEventListener("transitionend", function() {
-      self.alertTransitionOver();
-    }, true);
-
-    return this.container = container;
-  },
 
   showAlertNotification: function ah_show(aImageURL, aTitle, aText, aTextClickable, aCookie, aListener) {
-    this._clickable = aTextClickable || false;
-    this._listener = aListener || null;
-    this._cookie = aCookie || "";
-
-    // Reset the container settings from the last time so layout can happen naturally
-    let container = this.container;
-    container.removeAttribute("width");
-    let alertText = document.getElementById("alerts-text");
-    alertText.style.whiteSpace = "";
-
-    document.getElementById("alerts-image").setAttribute("src", aImageURL);
-    document.getElementById("alerts-title").value = aTitle;
-    alertText.textContent = aText;
+    Services.obs.addObserver(this, "metro_native_toast_clicked", false);
+    this._listener = aListener;
+    this._cookie = aCookie;
 
-    container.hidden = false;
-    let bcr = container.getBoundingClientRect();
-    if (bcr.width > window.innerWidth - 50) {
-      // If the window isn't wide enough, we need to re-layout
-      container.setAttribute("width", window.innerWidth - 50); // force a max width
-      alertText.style.whiteSpace = "pre-wrap"; // wrap text as needed
-      bcr = container.getBoundingClientRect(); // recalculate the bcr
-    }
-    container.setAttribute("width", bcr.width); // redundant but cheap
-    container.setAttribute("height", bcr.height);
-
-    container.classList.add("showing");
-
-    let timeout = Services.prefs.getIntPref("alerts.totalOpenTime");
-    let self = this;
-    if (this._timeoutID)
-      clearTimeout(this._timeoutID);
-    this._timeoutID = setTimeout(function() { self._timeoutAlert(); }, timeout);
+    MetroUtils.showNativeToast(aTitle, aText, aImageURL);
   },
 
-  _timeoutAlert: function ah__timeoutAlert() {
-    this._timeoutID = -1;
-
-    this.container.classList.remove("showing");
-    if (this._listener)
-      this._listener.observe(null, "alertfinished", this._cookie);
-  },
-
-  alertTransitionOver: function ah_alertTransitionOver() {
-    let container = this.container;
-    if (!container.classList.contains("showing")) {
-      container.height = 0;
-      container.hidden = true;
-    }
-  },
-
-  click: function ah_click(aEvent) {
-    if (this._clickable && this._listener)
-      this._listener.observe(null, "alertclickcallback", this._cookie);
-
-    if (this._timeoutID != -1) {
-      clearTimeout(this._timeoutID);
-      this._timeoutAlert();
+  observe: function(aSubject, aTopic, aData) {
+    switch (aTopic) {
+      case "metro_native_toast_clicked":
+        Services.obs.removeObserver(this, "metro_native_toast_clicked");
+        this._listener.observe(null, "alertclickcallback", this._cookie);
+        break;
     }
   }
 };
--- a/browser/metro/components/AlertsService.js
+++ b/browser/metro/components/AlertsService.js
@@ -15,13 +15,34 @@ Cu.import("resource://gre/modules/Servic
 function AlertsService() { }
 
 AlertsService.prototype = {
   classID: Components.ID("{fe33c107-82a4-41d6-8c64-5353267e04c9}"),
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIAlertsService]),
 
   showAlertNotification: function(aImageUrl, aTitle, aText, aTextClickable, aCookie, aAlertListener, aName) {
     let browser = Services.wm.getMostRecentWindow("navigator:browser");
-    browser.AlertsHelper.showAlertNotification(aImageUrl, aTitle, aText, aTextClickable, aCookie, aAlertListener);
+    try {
+      browser.AlertsHelper.showAlertNotification(aImageUrl, aTitle, aText, aTextClickable, aCookie, aAlertListener);
+    } catch (ex) {
+      let chromeWin = this._getChromeWindow(browser).wrappedJSObject;
+      let notificationBox = chromeWin.Browser.getNotificationBox();
+      notificationBox.appendNotification(aTitle,
+                                         aText,
+                                         aImageUrl,
+                                         notificationBox.PRIORITY_WARNING_MEDIUM,
+                                         null);
+    }
+  },
+
+  _getChromeWindow: function (aWindow) {
+      let chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+                            .getInterface(Ci.nsIWebNavigation)
+                            .QueryInterface(Ci.nsIDocShellTreeItem)
+                            .rootTreeItem
+                            .QueryInterface(Ci.nsIInterfaceRequestor)
+                            .getInterface(Ci.nsIDOMWindow)
+                            .QueryInterface(Ci.nsIDOMChromeWindow);
+     return chromeWin;
   }
 };
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AlertsService]);
--- a/browser/metro/locales/en-US/chrome/browser.properties
+++ b/browser/metro/locales/en-US/chrome/browser.properties
@@ -56,16 +56,17 @@ browserForSaveLocation=Save Location
 browserForOpenLocation=Open Location
 
 # Download Manager
 downloadsUnknownSize=Unknown size
 downloadRun=Run
 downloadSave=Save
 downloadCancel=Cancel
 downloadTryAgain=Try Again
+downloadRunNow=Run it now
 # LOCALIZATION NOTE (downloadShowInFiles): 'Files' refers to the Windows 8 file explorer
 downloadShowInFiles=Show in Files
 
 # Alerts
 alertLinkBookmarked=Bookmark added
 alertDownloads=Downloads
 alertDownloadsStart=Downloading: %S
 alertDownloadsDone=%S has finished downloading
--- a/browser/metro/theme/browser.css
+++ b/browser/metro/theme/browser.css
@@ -1240,42 +1240,9 @@ setting[type="radio"] > vbox {
 #clearprivacythrobber .progressBall {
   margin: 2px;
   width: 22px;
   height: 22px;
 }
 
 #clear-notification-done {
   font-weight: bold;
-}
-
-/* Alert Popup ============================================================= */
-
-#alerts-container {
-  color: white;
-  background-color: #5e6166;
-  border: @border_width_small@ solid #767973;
-  border-radius: @border_radius_normal@;
-  box-shadow: black 0 @border_radius_tiny@ @border_radius_tiny@;
-  padding: @padding_normal@; /* core spacing on top/bottom */
-  margin-bottom: @margin_large@;
-  transition-property: opacity;
-  transition-duration: 0.5s;
-  opacity: 0;
-}
-
-#alerts-container.showing {
-  opacity: 1;
-}
-
-#alerts-title {
-  font-size: @font_small@ !important;
-}
-
-#alerts-text {
-  font-size: @font_xsmall@ !important;
-  white-space: pre;
-}
-
-#alerts-container {
-  -moz-margin-end: @margin_large@;
-}
-
+}
\ No newline at end of file
--- a/widget/nsIWinMetroUtils.idl
+++ b/widget/nsIWinMetroUtils.idl
@@ -7,17 +7,17 @@
 
 /**
  * Integration with the "Metro"/"Modern" UI environment in Windows 8.
  *
  * Note: browser/metro/base/content/browser-scripts.js contains a stub
  * implementation of this interface for non-Windows systems, for testing and
  * development purposes only.
  */
-[scriptable, uuid(ac813696-3b0a-4259-bce1-1d078021ebbe)]
+[scriptable, uuid(c5a654c8-2443-47ce-9322-d150af3ca526)]
 interface nsIWinMetroUtils : nsISupports
 {
   /* Fullscreen landscape orientation */
   const long fullScreenLandscape = 0;
   /* Larger snapped state */
   const long filled = 1;
   /* Smaller snapped state */
   const long snapped = 2;
@@ -62,16 +62,21 @@ interface nsIWinMetroUtils : nsISupports
 
   /**
    * Launches the specified application with the specified arguments and
    * switches to Desktop mode if in metro mode.
    */
    void launchInDesktop(in AString aPath, in AString aArguments); 
 
   /**
+   * Displays a native Windows 8 toast.
+   */
+   void showNativeToast(in AString aTitle, in AString aMessage, in AString anImage);
+
+  /**
    * Secondary tiles are a Windows 8 specific feature for pinning new tiles
    * to the start screen.   Tiles can later be activated whether the browser is
    * already opened or not. 
    */
 
   /**
    * Pins a new tile to the Windows 8 start screen.
    * 
new file mode 100644
--- /dev/null
+++ b/widget/windows/winrt/ToastNotificationHandler.cpp
@@ -0,0 +1,78 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "MetroUtils.h"
+#include "mozilla/Services.h"
+#include "FrameworkView.h"
+
+using namespace ABI::Windows::Foundation;
+using namespace ABI::Windows::Data::Xml::Dom;
+using namespace Microsoft::WRL;
+using namespace Microsoft::WRL::Wrappers;
+using namespace mozilla;
+using namespace ABI::Windows::UI::Notifications;
+
+typedef __FITypedEventHandler_2_Windows__CUI__CNotifications__CToastNotification_IInspectable_t ToastActivationHandler;
+
+void ToastNotificationHandler::DisplayNotification(HSTRING title, HSTRING msg, HSTRING imagePath) {
+  ComPtr<IToastNotificationManagerStatics> toastNotificationManagerStatics;
+  AssertHRESULT(GetActivationFactory(HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotificationManager).Get(),
+                                    toastNotificationManagerStatics.GetAddressOf()));
+
+  ComPtr<IXmlDocument> toastXml;
+  toastNotificationManagerStatics->GetTemplateContent(ToastTemplateType::ToastTemplateType_ToastImageAndText03, &toastXml);
+
+  ComPtr<IXmlNodeList> toastTextElements, toastImageElements;
+  ComPtr<IXmlNode> titleTextNodeRoot, msgTextNodeRoot, imageNodeRoot, srcAttribute;
+
+  HSTRING textNodeStr, imageNodeStr, srcNodeStr;
+  HSTRING_HEADER textHeader, imageHeader, srcHeader;
+  WindowsCreateStringReference(L"text", 4, &textHeader, &textNodeStr);
+  WindowsCreateStringReference(L"image", 5, &imageHeader, &imageNodeStr);
+  WindowsCreateStringReference(L"src", 3, &srcHeader, &srcNodeStr);
+  toastXml->GetElementsByTagName(textNodeStr, &toastTextElements);
+  toastXml->GetElementsByTagName(imageNodeStr, &toastImageElements);
+
+  AssertHRESULT(toastTextElements->Item(0, &titleTextNodeRoot));
+  AssertHRESULT(toastTextElements->Item(1, &msgTextNodeRoot));
+  AssertHRESULT(toastImageElements->Item(0, &imageNodeRoot));
+
+  ComPtr<IXmlNamedNodeMap> attributes;
+  AssertHRESULT(imageNodeRoot->get_Attributes(&attributes));
+  AssertHRESULT(attributes->GetNamedItem(srcNodeStr, &srcAttribute));
+
+  SetNodeValueString(title, titleTextNodeRoot.Get(), toastXml.Get());
+  SetNodeValueString(msg, msgTextNodeRoot.Get(), toastXml.Get());
+  SetNodeValueString(imagePath, srcAttribute.Get(), toastXml.Get());
+
+  ComPtr<IToastNotification> notification;
+  ComPtr<IToastNotificationFactory> factory;
+  AssertHRESULT(GetActivationFactory(HStringReference(RuntimeClass_Windows_UI_Notifications_ToastNotification).Get(),
+                factory.GetAddressOf()));
+  AssertHRESULT(factory->CreateToastNotification(toastXml.Get(), &notification));
+
+  EventRegistrationToken activatedToken;
+  AssertHRESULT(notification->add_Activated(Callback<ToastActivationHandler>(this,
+      &ToastNotificationHandler::OnActivate).Get(), &activatedToken));
+
+  ComPtr<IToastNotifier> notifier;
+  toastNotificationManagerStatics->CreateToastNotifier(&notifier);
+  notifier->Show(notification.Get());
+}
+
+void ToastNotificationHandler::SetNodeValueString(HSTRING inputString, ComPtr<IXmlNode> node, ComPtr<IXmlDocument> xml) { 
+  ComPtr<IXmlText> inputText;
+  ComPtr<IXmlNode> inputTextNode, pAppendedChild;
+
+  AssertHRESULT(xml->CreateTextNode(inputString, &inputText));
+  AssertHRESULT(inputText.As(&inputTextNode));
+  AssertHRESULT(node->AppendChild(inputTextNode.Get(), &pAppendedChild));
+}
+
+HRESULT ToastNotificationHandler::OnActivate(IToastNotification *notification, IInspectable *inspectable) {
+  MetroUtils::FireObserver("metro_native_toast_clicked");
+  return S_OK;
+}
new file mode 100644
--- /dev/null
+++ b/widget/windows/winrt/ToastNotificationHandler.h
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#pragma once
+
+#include <windows.ui.notifications.h>
+#include <windows.data.xml.dom.h>
+#include "mozwrlbase.h"
+
+using namespace Microsoft::WRL;
+
+class ToastNotificationHandler {
+    typedef ABI::Windows::UI::Notifications::IToastNotification IToastNotification;
+    typedef ABI::Windows::Data::Xml::Dom::IXmlNode IXmlNode;
+    typedef ABI::Windows::Data::Xml::Dom::IXmlDocument IXmlDocument;
+
+    void SetNodeValueString(HSTRING inputString, ComPtr<IXmlNode> node, ComPtr<IXmlDocument> xml);
+  public:
+    ToastNotificationHandler() {};
+    ~ToastNotificationHandler() {};
+
+    void DisplayNotification(HSTRING title, HSTRING msg, HSTRING imagePath);
+    HRESULT OnActivate(IToastNotification *notification, IInspectable *inspectable);
+};
--- a/widget/windows/winrt/moz.build
+++ b/widget/windows/winrt/moz.build
@@ -10,16 +10,17 @@ CPP_SOURCES += [
     'FrameworkView.cpp',
     'FrameworkViewGfx.cpp',
     'MetroApp.cpp',
     'MetroAppShell.cpp',
     'MetroContracts.cpp',
     'MetroInput.cpp',
     'MetroUtils.cpp',
     'MetroWidget.cpp',
+    'ToastNotificationHandler.cpp',
     'UIAAccessibilityBridge.cpp',
     'UIABridge.cpp',
     'nsMetroFilePicker.cpp',
     'nsWinMetroUtils.cpp',
 ]
 
 EXTRA_COMPONENTS += [
     'MetroUIUtils.js',
--- a/widget/windows/winrt/nsWinMetroUtils.cpp
+++ b/widget/windows/winrt/nsWinMetroUtils.cpp
@@ -4,16 +4,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "nsWinMetroUtils.h"
 #include "MetroUtils.h"
 #include "nsXULAppAPI.h"
 #include "FrameworkView.h"
 #include "MetroApp.h"
 #include "nsIWindowsRegKey.h"
+#include "ToastNotificationHandler.h"
 
 #include <shldisp.h>
 #include <shellapi.h>
 #include <windows.ui.viewmanagement.h>
 #include <windows.ui.startscreen.h>
 #include <Wincrypt.h>
 
 using namespace ABI::Windows::Foundation;
@@ -334,16 +335,34 @@ nsWinMetroUtils::LaunchInDesktop(const n
   sinfo.lpFile       = aPath.BeginReading();
   sinfo.lpParameters = aArguments.BeginReading();
   sinfo.lpVerb       = L"open";
   sinfo.nShow        = SW_SHOWNORMAL;
 
   if (!ShellExecuteEx(&sinfo)) {
     return NS_ERROR_FAILURE;
   }
+}
+
+NS_IMETHODIMP
+nsWinMetroUtils::ShowNativeToast(const nsAString &aTitle,
+  const nsAString &aMessage, const nsAString &anImage)
+{
+  // Firefox is in the foreground, no need for a notification.
+  if (::GetActiveWindow() == ::GetForegroundWindow()) {
+    return NS_OK;
+  }
+
+  ToastNotificationHandler* notification_handler =
+      new ToastNotificationHandler;
+
+  HSTRING title = HStringReference(aTitle.BeginReading()).Get();
+  HSTRING msg = HStringReference(aMessage.BeginReading()).Get();
+  HSTRING imagePath = HStringReference(anImage.BeginReading()).Get();
+  notification_handler->DisplayNotification(title, msg, imagePath);
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsWinMetroUtils::GetSnappedState(int32_t *aSnappedState)
 {
   if (XRE_GetWindowsEnvironment() == WindowsEnvironmentType_Desktop) {