Bug 1254291 - Add schema for the notifications API, r=kmag
☠☠ backed out by a4c6d6add2fc ☠ ☠
authorbsilverberg <bsilverberg@mozilla.com>
Tue, 15 Mar 2016 17:36:54 +0100
changeset 288938 e5c63c3fb088c0f47becdbf3b2afa5e9576dd9d3
parent 288937 c715f7a518276c10ef3ace02ea374a511312ac8a
child 288939 72cd7ab94678b6bc2e607702cc398a378a1787a8
push id18203
push usercbook@mozilla.com
push dateWed, 16 Mar 2016 15:54:19 +0000
treeherderfx-team@72cd7ab94678 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1254291
milestone48.0a1
Bug 1254291 - Add schema for the notifications API, r=kmag Also update ext-notifications.js to use a Map instead of a Set Note that I am not sure whether the schema has been implemented exactly as we'd want. There are, for example, options in NotificationOptions that we will never use/support, so I'm not sure how to document that. MozReview-Commit-ID: 5Di7klOdI6G
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ext-notifications.js
toolkit/components/extensions/schemas/jar.mn
toolkit/components/extensions/schemas/notifications.json
toolkit/components/extensions/test/mochitest/test_ext_notifications.html
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -77,16 +77,17 @@ const BASE_SCHEMA = "chrome://extensions
 
 ExtensionManagement.registerSchema("chrome://extensions/content/schemas/alarms.json");
 ExtensionManagement.registerSchema("chrome://extensions/content/schemas/cookies.json");
 ExtensionManagement.registerSchema("chrome://extensions/content/schemas/downloads.json");
 ExtensionManagement.registerSchema("chrome://extensions/content/schemas/extension.json");
 ExtensionManagement.registerSchema("chrome://extensions/content/schemas/extension_types.json");
 ExtensionManagement.registerSchema("chrome://extensions/content/schemas/i18n.json");
 ExtensionManagement.registerSchema("chrome://extensions/content/schemas/idle.json");
+ExtensionManagement.registerSchema("chrome://extensions/content/schemas/notifications.json");
 ExtensionManagement.registerSchema("chrome://extensions/content/schemas/runtime.json");
 ExtensionManagement.registerSchema("chrome://extensions/content/schemas/storage.json");
 ExtensionManagement.registerSchema("chrome://extensions/content/schemas/test.json");
 ExtensionManagement.registerSchema("chrome://extensions/content/schemas/web_navigation.json");
 ExtensionManagement.registerSchema("chrome://extensions/content/schemas/web_request.json");
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
--- a/toolkit/components/extensions/ext-notifications.js
+++ b/toolkit/components/extensions/ext-notifications.js
@@ -3,17 +3,17 @@
 var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/ExtensionUtils.jsm");
 var {
   EventManager,
   ignoreEvent,
 } = ExtensionUtils;
 
-// WeakMap[Extension -> Set[Notification]]
+// WeakMap[Extension -> Map[id -> Notification]]
 var notificationsMap = new WeakMap();
 
 // WeakMap[Extension -> Set[callback]]
 var notificationCallbacksMap = new WeakMap();
 
 // Manages a notification popup (notifications API) created by the extension.
 function Notification(extension, id, options) {
   this.extension = extension;
@@ -42,90 +42,82 @@ function Notification(extension, id, opt
 Notification.prototype = {
   clear() {
     try {
       let svc = Cc["@mozilla.org/alerts-service;1"].getService(Ci.nsIAlertsService);
       svc.closeAlert(this.id);
     } catch (e) {
       // This will fail if the OS doesn't support this function.
     }
-    notificationsMap.get(this.extension).delete(this);
+    notificationsMap.get(this.extension).delete(this.id);
   },
 
   observe(subject, topic, data) {
     if (topic != "alertfinished") {
       return;
     }
 
     for (let callback of notificationCallbacksMap.get(this.extension)) {
       callback(this);
     }
 
-    notificationsMap.get(this.extension).delete(this);
+    notificationsMap.get(this.extension).delete(this.id);
   },
 };
 
 /* eslint-disable mozilla/balanced-listeners */
 extensions.on("startup", (type, extension) => {
-  notificationsMap.set(extension, new Set());
+  notificationsMap.set(extension, new Map());
   notificationCallbacksMap.set(extension, new Set());
 });
 
 extensions.on("shutdown", (type, extension) => {
-  for (let notification of notificationsMap.get(extension)) {
+  for (let notification of notificationsMap.get(extension).values()) {
     notification.clear();
   }
   notificationsMap.delete(extension);
   notificationCallbacksMap.delete(extension);
 });
 /* eslint-enable mozilla/balanced-listeners */
 
 var nextId = 0;
 
-extensions.registerPrivilegedAPI("notifications", (extension, context) => {
+extensions.registerSchemaAPI("notifications", "notifications", (extension, context) => {
   return {
     notifications: {
-      create: function(...args) {
-        let notificationId, options, callback;
-        if (args.length == 1) {
-          options = args[0];
-        } else {
-          [notificationId, options, callback] = args;
+      create: function(notificationId, options) {
+        if (!notificationId) {
+          notificationId = String(nextId++);
         }
 
-        if (!notificationId) {
-          notificationId = nextId++;
+        let notifications = notificationsMap.get(extension);
+        if (notifications.has(notificationId)) {
+          notifications.get(notificationId).clear();
         }
 
         // FIXME: Lots of options still aren't supported, especially
         // buttons.
         let notification = new Notification(extension, notificationId, options);
-        notificationsMap.get(extension).add(notification);
+        notificationsMap.get(extension).set(notificationId, notification);
 
-        return context.wrapPromise(Promise.resolve(notificationId), callback);
+        return Promise.resolve(notificationId);
       },
 
-      clear: function(notificationId, callback) {
+      clear: function(notificationId) {
         let notifications = notificationsMap.get(extension);
-        let cleared = false;
-        for (let notification of notifications) {
-          if (notification.id == notificationId) {
-            notification.clear();
-            cleared = true;
-            break;
-          }
+        if (notifications.has(notificationId)) {
+          notifications.get(notificationId).clear();
+          return Promise.resolve(true);
         }
-
-        return context.wrapPromise(Promise.resolve(cleared), callback);
+        return Promise.resolve(false);
       },
 
-      getAll: function(callback) {
-        let notifications = notificationsMap.get(extension);
-        notifications = Array.from(notifications, notification => notification.id);
-        return context.wrapPromise(Promise.resolve(notifications), callback);
+      getAll: function() {
+        let result = Array.from(notificationsMap.get(extension).keys());
+        return Promise.resolve(result);
       },
 
       onClosed: new EventManager(context, "notifications.onClosed", fire => {
         let listener = notification => {
           // FIXME: Support the byUser argument.
           fire(notification.id, true);
         };
 
--- a/toolkit/components/extensions/schemas/jar.mn
+++ b/toolkit/components/extensions/schemas/jar.mn
@@ -7,13 +7,14 @@ toolkit.jar:
     content/extensions/schemas/alarms.json
     content/extensions/schemas/cookies.json
     content/extensions/schemas/downloads.json
     content/extensions/schemas/extension.json
     content/extensions/schemas/extension_types.json
     content/extensions/schemas/i18n.json
     content/extensions/schemas/idle.json
     content/extensions/schemas/manifest.json
+    content/extensions/schemas/notifications.json
     content/extensions/schemas/runtime.json
     content/extensions/schemas/storage.json
     content/extensions/schemas/test.json
     content/extensions/schemas/web_navigation.json
     content/extensions/schemas/web_request.json
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/schemas/notifications.json
@@ -0,0 +1,380 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+[
+  {
+    "namespace": "notifications",
+    "types": [
+      {
+        "id": "TemplateType",
+        "type": "string",
+        "enum": [
+          "basic",
+          "image",
+          "list",
+          "progress"
+        ]
+      },
+      {
+        "id": "PermissionLevel",
+        "type": "string",
+        "enum": [
+          "granted",
+          "denied"
+        ]
+      },
+      {
+        "id": "NotificationItem",
+        "type": "object",
+        "properties": {
+          "title": {
+            "description": "Title of one item of a list notification.",
+            "type": "string"
+          },
+          "message": {
+            "description": "Additional details about this item.",
+            "type": "string"
+          }
+        }
+      },
+      {
+        "id": "CreateNotificationOptions",
+        "type": "object",
+        "properties": {
+          "type": {
+            "description": "Which type of notification to display.",
+            "$ref": "TemplateType"
+          },
+          "iconUrl": {
+            "description": "A URL to the sender's avatar, app icon, or a thumbnail for image notifications.",
+            "type": "string"
+          },
+          "appIconMaskUrl": {
+            "optional": true,
+            "description": "A URL to the app icon mask.",
+            "type": "string"
+          },
+          "title": {
+            "description": "Title of the notification (e.g. sender name for email).",
+            "type": "string"
+          },
+          "message": {
+            "description": "Main notification content.",
+            "type": "string"
+          },
+          "contextMessage": {
+            "optional": true,
+            "description": "Alternate notification content with a lower-weight font.",
+            "type": "string"
+          },
+          "priority": {
+            "optional": true,
+            "description": "Priority ranges from -2 to 2. -2 is lowest priority. 2 is highest. Zero is default.",
+            "type": "integer",
+            "minimum": -2,
+            "maximum": 2
+          },
+          "eventTime": {
+            "optional": true,
+            "description": "A timestamp associated with the notification, in milliseconds past the epoch.",
+            "type": "number"
+          },
+          "imageUrl": {
+            "optional": true,
+            "description": "A URL to the image thumbnail for image-type notifications.",
+            "type": "string"
+          },
+          "items": {
+            "optional": true,
+            "description": "Items for multi-item notifications.",
+            "type": "array",
+            "items": { "$ref": "NotificationItem" }
+          },
+          "progress": {
+            "optional": true,
+            "description": "Current progress ranges from 0 to 100.",
+            "type": "integer",
+            "minimum": 0,
+            "maximum": 100
+          },
+          "isClickable": {
+            "optional": true,
+            "description": "Whether to show UI indicating that the app will visibly respond to clicks on the body of a notification.",
+            "type": "boolean"
+          }
+        }
+      },
+      {
+        "id": "UpdateNotificationOptions",
+        "type": "object",
+        "properties": {
+          "type": {
+            "optional": true,
+            "description": "Which type of notification to display.",
+            "$ref": "TemplateType"
+          },
+          "iconUrl": {
+            "optional": true,
+            "description": "A URL to the sender's avatar, app icon, or a thumbnail for image notifications.",
+            "type": "string"
+          },
+          "appIconMaskUrl": {
+            "optional": true,
+            "description": "A URL to the app icon mask.",
+            "type": "string"
+          },
+          "title": {
+            "optional": true,
+            "description": "Title of the notification (e.g. sender name for email).",
+            "type": "string"
+          },
+          "message": {
+            "optional": true,
+            "description": "Main notification content.",
+            "type": "string"
+          },
+          "contextMessage": {
+            "optional": true,
+            "description": "Alternate notification content with a lower-weight font.",
+            "type": "string"
+          },
+          "priority": {
+            "optional": true,
+            "description": "Priority ranges from -2 to 2. -2 is lowest priority. 2 is highest. Zero is default.",
+            "type": "integer",
+            "minimum": -2,
+            "maximum": 2
+          },
+          "eventTime": {
+            "optional": true,
+            "description": "A timestamp associated with the notification, in milliseconds past the epoch.",
+            "type": "number"
+          },
+          "imageUrl": {
+            "optional": true,
+            "description": "A URL to the image thumbnail for image-type notifications.",
+            "type": "string"
+          },
+          "items": {
+            "optional": true,
+            "description": "Items for multi-item notifications.",
+            "type": "array",
+            "items": { "$ref": "NotificationItem" }
+          },
+          "progress": {
+            "optional": true,
+            "description": "Current progress ranges from 0 to 100.",
+            "type": "integer",
+            "minimum": 0,
+            "maximum": 100
+          },
+          "isClickable": {
+            "optional": true,
+            "description": "Whether to show UI indicating that the app will visibly respond to clicks on the body of a notification.",
+            "type": "boolean"
+          }
+        }
+      }
+    ],
+    "functions": [
+      {
+        "name": "create",
+        "type": "function",
+        "description": "Creates and displays a notification.",
+        "async": "callback",
+        "parameters": [
+          {
+            "type": "string",
+            "name": "notificationId",
+            "description": "Identifier of the notification. If it is empty, this method generates an id. If it matches an existing notification, this method first clears that notification before proceeding with the create operation."
+          },
+          {
+            "$ref": "CreateNotificationOptions",
+            "name": "options",
+            "description": "Contents of the notification."
+          },
+          {
+            "optional": true,
+            "type": "function",
+            "name": "callback",
+            "parameters": [
+              {
+                "name": "notificationId",
+                "type": "string",
+                "description": "The notification id (either supplied or generated) that represents the created notification."
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "name": "update",
+        "unsupported": true,
+        "type": "function",
+        "description": "Updates an existing notification.",
+        "async": "callback",
+        "parameters": [
+          {
+            "type": "string",
+            "name": "notificationId",
+            "description": "The id of the notification to be updated."
+          },
+          {
+            "$ref": "UpdateNotificationOptions",
+            "name": "options",
+            "description": "Contents of the notification to update to."
+          },
+          {
+            "optional": true,
+            "type": "function",
+            "name": "callback",
+            "parameters": [
+              {
+                "name": "wasUpdated",
+                "type": "boolean",
+                "description": "Indicates whether a matching notification existed."
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "name": "clear",
+        "type": "function",
+        "description": "Clears an existing notification.",
+        "async": "callback",
+        "parameters": [
+          {
+            "type": "string",
+            "name": "notificationId",
+            "description": "The id of the notification to be updated."
+          },
+          {
+            "optional": true,
+            "type": "function",
+            "name": "callback",
+            "parameters": [
+              {
+                "name": "wasCleared",
+                "type": "boolean",
+                "description": "Indicates whether a matching notification existed."
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "name": "getAll",
+        "type": "function",
+        "description": "Retrieves all the notifications.",
+        "async": "callback",
+        "parameters": [
+          {
+            "type": "function",
+            "name": "callback",
+            "parameters": [
+              {
+                "name": "notifications",
+                "type": "array",
+                "description": "The set of notification_ids currently in the system.",
+                "items": {
+                  "type": "string"
+                }
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "name": "getPermissionLevel",
+        "unsupported": true,
+        "type": "function",
+        "description": "Retrieves whether the user has enabled notifications from this app or extension.",
+        "async": "callback",
+        "parameters": [
+          {
+            "type": "function",
+            "name": "callback",
+            "parameters": [
+              {
+                "name": "level",
+                "$ref": "PermissionLevel",
+                "description": "The current permission level."
+              }
+            ]
+          }
+        ]
+      }
+    ],
+    "events": [
+      {
+        "name": "onClosed",
+        "type": "function",
+        "description": "Fired when the notification closed, either by the system or by user action.",
+        "parameters": [
+          {
+            "type": "string",
+            "name": "notificationId",
+            "description": "The notificationId of the closed notification."
+          },
+          {
+            "type": "boolean",
+            "name": "byUser",
+            "description": "True if the notification was closed by the user."
+          }
+        ]
+      },
+      {
+        "name": "onClicked",
+        "type": "function",
+        "description": "Fired when the user clicked in a non-button area of the notification.",
+        "parameters": [
+          {
+            "type": "string",
+            "name": "notificationId",
+            "description": "The notificationId of the clicked notification."
+          }
+        ]
+      },
+      {
+        "name": "onButtonClicked",
+        "type": "function",
+        "description": "Fired when the  user pressed a button in the notification.",
+        "parameters": [
+          {
+            "type": "string",
+            "name": "notificationId",
+            "description": "The notificationId of the clicked notification."
+          },
+          {
+            "type": "number",
+            "name": "buttonIndex",
+            "description": "The index of the button clicked by the user."
+          }
+        ]
+      },
+      {
+        "name": "onPermissionLevelChanged",
+        "unsupported": true,
+        "type": "function",
+        "description": "Fired when the user changes the permission level.",
+        "parameters": [
+          {
+            "$ref": "PermissionLevel",
+            "name": "level",
+            "description": "The new permission level."
+          }
+        ]
+      },
+      {
+        "name": "onShowSettings",
+        "unsupported": true,
+        "type": "function",
+        "description": "Fired when the user clicked on a link for the app's notification settings.",
+        "parameters": [
+        ]
+      }
+    ]
+  }
+]
--- a/toolkit/components/extensions/test/mochitest/test_ext_notifications.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_notifications.html
@@ -11,22 +11,27 @@
 
 <script type="text/javascript">
 "use strict";
 
 add_task(function* test_notifications() {
   function backgroundScript() {
     browser.test.log("running background script");
 
-    let opts = {title: "Testing Notification", message: "Carry on"};
+    let opts = {
+      type: "basic",
+      iconUrl: browser.runtime.getURL("data/a.png"),
+      title: "Testing Notification",
+      message: "Carry on",
+    };
 
     // Test an unimplemented listener.
     browser.notifications.onClicked.addListener(function() {});
 
-    browser.notifications.create("5", opts, function(id) {
+    browser.notifications.create("5", opts).then(id => {
       browser.test.sendMessage("running", id);
       browser.test.notifyPass("background test passed");
     });
   }
 
   let extensionData = {
     manifest: {
       permissions: ["notifications"],
@@ -45,17 +50,17 @@ add_task(function* test_notifications() 
   yield extension.unload();
   info("extension unloaded successfully");
 });
 
 add_task(function* test_notifications_empty_getAll() {
   function backgroundScript() {
     browser.test.log("running background script");
 
-    browser.notifications.getAll(notifications => {
+    browser.notifications.getAll().then(notifications => {
       browser.test.assertTrue(Array.isArray(notifications),
         "getAll() returned an array");
       browser.test.assertEq(notifications.length, 0, "the array was empty");
       browser.test.notifyPass("getAll empty");
     });
   }
 
   let extensionData = {
@@ -74,31 +79,37 @@ add_task(function* test_notifications_em
   yield extension.unload();
   info("extension unloaded successfully");
 });
 
 add_task(function* test_notifications_populated_getAll() {
   function backgroundScript() {
     browser.test.log("running background script");
 
-    let opts = {title: "Testing Notification", message: "Carry on"};
-    browser.notifications.create("p1", opts, () => {
-      browser.notifications.create("p2", opts, () => {
-        browser.notifications.getAll(notifications => {
-          browser.test.assertTrue(Array.isArray(notifications),
-            "getAll() returned an array");
-          browser.test.assertEq(notifications.length, 2,
-            "the array contained two notification ids");
-          browser.test.assertTrue(notifications.includes("p1"),
-            "the array contains the first notification");
-          browser.test.assertTrue(notifications.includes("p2"),
-            "the array contains the second notification");
-          browser.test.notifyPass("getAll populated");
-        });
-      });
+    let opts = {
+      type: "basic",
+      iconUrl: browser.runtime.getURL("data/a.png"),
+      title: "Testing Notification",
+      message: "Carry on",
+    };
+
+    browser.notifications.create("p1", opts).then(() => {
+      return browser.notifications.create("p2", opts);
+    }).then(() => {
+      return browser.notifications.getAll();
+    }).then(notifications => {
+      browser.test.assertTrue(Array.isArray(notifications),
+        "getAll() returned an array");
+      browser.test.assertEq(notifications.length, 2,
+        "the array contained two notification ids");
+      browser.test.assertTrue(notifications.includes("p1"),
+        "the array contains the first notification");
+      browser.test.assertTrue(notifications.includes("p2"),
+        "the array contains the second notification");
+      browser.test.notifyPass("getAll populated");
     });
   }
 
   let extensionData = {
     manifest: {
       permissions: ["notifications"],
     },
     background: "(" + backgroundScript.toString() + ")()",