Bug 1339610 - Web extension API for container icon and colors. r?baku r?kmag draft
authorJonathan Kingston <jkt@mozilla.com>
Sun, 27 Aug 2017 00:47:02 +0100
changeset 659164 e3d9c852a8410beb6c606e4fc2febab6a76590dc
parent 658007 1a6d7fb15a648ee54a826c68e5ade9f86bf4ce58
child 729912 81fa7b1e97fb59b8fd092b04abdf7f15d5562c28
push id78045
push userbmo:jkt@mozilla.com
push dateTue, 05 Sep 2017 17:09:24 +0000
reviewersbaku, kmag
bugs1339610
milestone57.0a1
Bug 1339610 - Web extension API for container icon and colors. r?baku r?kmag MozReview-Commit-ID: BosKoxM8FMZ
toolkit/components/extensions/ext-contextualIdentities.js
toolkit/components/extensions/schemas/contextual_identities.json
toolkit/components/extensions/test/xpcshell/test_ext_contextual_identities.js
--- a/toolkit/components/extensions/ext-contextualIdentities.js
+++ b/toolkit/components/extensions/ext-contextualIdentities.js
@@ -5,42 +5,98 @@
 
 XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService",
                                   "resource://gre/modules/ContextualIdentityService.jsm");
 XPCOMUtils.defineLazyPreferenceGetter(this, "containersEnabled",
                                       "privacy.userContext.enabled");
 
 Cu.import("resource://gre/modules/ExtensionPreferencesManager.jsm");
 
+var {
+  ExtensionError,
+} = ExtensionUtils;
+
 const CONTAINER_PREF_INSTALL_DEFAULTS = {
   "privacy.userContext.enabled": true,
   "privacy.userContext.longPressBehavior": 2,
   "privacy.userContext.ui.enabled": true,
   "privacy.usercontext.about_newtab_segregation.enabled": true,
 };
 
 const CONTAINERS_ENABLED_SETTING_NAME = "privacy.containers";
 
+const CONTAINER_COLORS = new Map([
+  ["blue", "#37adff"],
+  ["turquoise", "#00c79a"],
+  ["green", "#51cd00"],
+  ["yellow", "#ffcb00"],
+  ["orange", "#ff9f00"],
+  ["red", "#ff613d"],
+  ["pink", "#ff4bda"],
+  ["purple", "#af51f5"],
+]);
+
+const CONTAINER_ICONS = new Set([
+  "briefcase",
+  "cart",
+  "circle",
+  "dollar",
+  "fingerprint",
+  "gift",
+  "vacation",
+  "food",
+  "fruit",
+  "pet",
+  "tree",
+  "chill",
+]);
+
+function getContainerIcon(iconName) {
+  if (!CONTAINER_ICONS.has(iconName)) {
+    throw new ExtensionError(`Invalid icon ${iconName} for container`);
+  }
+  return `resource://usercontext-content/${iconName}.svg`;
+}
+
+function getContainerColor(colorName) {
+  if (!CONTAINER_COLORS.has(colorName)) {
+    throw new ExtensionError(`Invalid color name ${colorName} for container`);
+  }
+  return CONTAINER_COLORS.get(colorName);
+}
+
 const convertIdentity = identity => {
   let result = {
     name: ContextualIdentityService.getUserContextLabel(identity.userContextId),
     icon: identity.icon,
+    iconUrl: getContainerIcon(identity.icon),
     color: identity.color,
+    colorCode: getContainerColor(identity.color),
     cookieStoreId: getCookieStoreIdForContainer(identity.userContextId),
   };
 
   return result;
 };
 
 const convertIdentityFromObserver = wrappedIdentity => {
   let identity = wrappedIdentity.wrappedJSObject;
+  let iconUrl, colorCode;
+  try {
+    iconUrl = getContainerIcon(identity.icon);
+    colorCode = getContainerColor(identity.color);
+  } catch (e) {
+    return null;
+  }
+
   let result = {
     name: identity.name,
     icon: identity.icon,
+    iconUrl,
     color: identity.color,
+    colorCode,
     cookieStoreId: getCookieStoreIdForContainer(identity.userContextId),
   };
 
   return result;
 };
 
 ExtensionPreferencesManager.addSetting(CONTAINERS_ENABLED_SETTING_NAME, {
   prefNames: Object.keys(CONTAINER_PREF_INSTALL_DEFAULTS),
@@ -65,47 +121,51 @@ this.contextualIdentities = class extend
     if (extension.hasPermission("contextualIdentities")) {
       ExtensionPreferencesManager.setSetting(extension, CONTAINERS_ENABLED_SETTING_NAME, true);
     }
   }
 
   getAPI(context) {
     let self = {
       contextualIdentities: {
-        get(cookieStoreId) {
+        async get(cookieStoreId) {
           let containerId = getContainerForCookieStoreId(cookieStoreId);
           if (!containerId) {
             return Promise.reject({
               message: `Invalid contextual identitiy: ${cookieStoreId}`,
             });
           }
 
           let identity = ContextualIdentityService.getPublicIdentityFromId(containerId);
-          return Promise.resolve(convertIdentity(identity));
+          return convertIdentity(identity);
         },
 
-        query(details) {
+        async query(details) {
           let identities = [];
           ContextualIdentityService.getPublicIdentities().forEach(identity => {
             if (details.name &&
                 ContextualIdentityService.getUserContextLabel(identity.userContextId) != details.name) {
               return;
             }
 
             identities.push(convertIdentity(identity));
           });
 
-          return Promise.resolve(identities);
+          return identities;
         },
 
-        create(details) {
+        async create(details) {
+          // Lets prevent making containers that are not valid
+          getContainerIcon(details.icon);
+          getContainerColor(details.color);
+
           let identity = ContextualIdentityService.create(details.name,
                                                           details.icon,
                                                           details.color);
-          return Promise.resolve(convertIdentity(identity));
+          return convertIdentity(identity);
         },
 
         update(cookieStoreId, details) {
           let containerId = getContainerForCookieStoreId(cookieStoreId);
           if (!containerId) {
             return Promise.reject({
               message: `Invalid contextual identitiy: ${cookieStoreId}`,
             });
@@ -133,20 +193,20 @@ this.contextualIdentities = class extend
           if (!ContextualIdentityService.update(identity.userContextId,
                                                 identity.name, identity.icon,
                                                 identity.color)) {
             return Promise.reject({
               message: `Contextual identitiy failed to update: ${cookieStoreId}`,
             });
           }
 
-          return Promise.resolve(convertIdentity(identity));
+          return convertIdentity(identity);
         },
 
-        remove(cookieStoreId) {
+        async remove(cookieStoreId) {
           let containerId = getContainerForCookieStoreId(cookieStoreId);
           if (!containerId) {
             return Promise.reject({
               message: `Invalid contextual identitiy: ${cookieStoreId}`,
             });
           }
 
           let identity = ContextualIdentityService.getPublicIdentityFromId(containerId);
@@ -160,44 +220,53 @@ this.contextualIdentities = class extend
           let convertedIdentity = convertIdentity(identity);
 
           if (!ContextualIdentityService.remove(identity.userContextId)) {
             return Promise.reject({
               message: `Contextual identitiy failed to remove: ${cookieStoreId}`,
             });
           }
 
-          return Promise.resolve(convertedIdentity);
+          return convertedIdentity;
         },
 
         onCreated: new EventManager(context, "contextualIdentities.onCreated", fire => {
           let observer = (subject, topic) => {
-            fire.async({contextualIdentity: convertIdentityFromObserver(subject)});
+            let convertedIdentity = convertIdentityFromObserver(subject);
+            if (convertedIdentity) {
+              fire.async({contextualIdentity: convertedIdentity});
+            }
           };
 
           Services.obs.addObserver(observer, "contextual-identity-created");
           return () => {
             Services.obs.removeObserver(observer, "contextual-identity-created");
           };
         }).api(),
 
         onUpdated: new EventManager(context, "contextualIdentities.onUpdated", fire => {
           let observer = (subject, topic) => {
-            fire.async({contextualIdentity: convertIdentityFromObserver(subject)});
+            let convertedIdentity = convertIdentityFromObserver(subject);
+            if (convertedIdentity) {
+              fire.async({contextualIdentity: convertedIdentity});
+            }
           };
 
           Services.obs.addObserver(observer, "contextual-identity-updated");
           return () => {
             Services.obs.removeObserver(observer, "contextual-identity-updated");
           };
         }).api(),
 
         onRemoved: new EventManager(context, "contextualIdentities.onRemoved", fire => {
           let observer = (subject, topic) => {
-            fire.async({contextualIdentity: convertIdentityFromObserver(subject)});
+            let convertedIdentity = convertIdentityFromObserver(subject);
+            if (convertedIdentity) {
+              fire.async({contextualIdentity: convertedIdentity});
+            }
           };
 
           Services.obs.addObserver(observer, "contextual-identity-deleted");
           return () => {
             Services.obs.removeObserver(observer, "contextual-identity-deleted");
           };
         }).api(),
 
--- a/toolkit/components/extensions/schemas/contextual_identities.json
+++ b/toolkit/components/extensions/schemas/contextual_identities.json
@@ -23,18 +23,20 @@
     "permissions": ["contextualIdentities"],
     "types": [
       {
         "id": "ContextualIdentity",
         "type": "object",
         "description": "Represents information about a contextual identity.",
         "properties": {
           "name": {"type": "string", "description": "The name of the contextual identity."},
-          "icon": {"type": "string", "description": "The icon of the contextual identity."},
-          "color": {"type": "string", "description": "The color of the contextual identity."},
+          "icon": {"type": "string", "description": "The icon name of the contextual identity."},
+          "iconUrl": {"type": "string", "description": "The icon url of the contextual identity."},
+          "color": {"type": "string", "description": "The color name of the contextual identity."},
+          "colorCode": {"type": "string", "description": "The color hash of the contextual identity."},
           "cookieStoreId": {"type": "string", "description": "The cookie store ID of the contextual identity."}
         }
       }
     ],
     "functions": [
       {
         "name": "get",
         "type": "function",
--- a/toolkit/components/extensions/test/xpcshell/test_ext_contextual_identities.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contextual_identities.js
@@ -46,34 +46,40 @@ add_task(async function test_contextualI
           browser.contextualIdentities[type].addListener(listener);
         } catch (e) {
           reject(e);
         }
       });
     }
 
     function assertExpected(expected, container) {
+      // Number of keys that are added by the APIs
+      const createdCount = 2;
       for (let key of Object.keys(container)) {
         browser.test.assertTrue(key in expected, `found property ${key}`);
         browser.test.assertEq(expected[key], container[key], `property value for ${key} is correct`);
       }
-      browser.test.assertEq(Object.keys(expected).length, Object.keys(container).length, "all expected properties found");
+      const hexMatch = /^#[0-9a-f]{6}$/;
+      browser.test.assertTrue(hexMatch.test(expected.colorCode), "Color code property was expected Hex shape");
+      const iconMatch = /^resource:\/\/usercontext-content\/[a-z]+[.]svg$/;
+      browser.test.assertTrue(iconMatch.test(expected.iconUrl), "Icon url property was expected shape");
+      browser.test.assertEq(Object.keys(expected).length, Object.keys(container).length + createdCount, "all expected properties found");
     }
 
     let onCreatePromise = createOneTimeListener("onCreated");
 
-    let containerObj = {name: "foobar", color: "red", icon: "icon"};
+    let containerObj = {name: "foobar", color: "red", icon: "circle"};
     let ci = await browser.contextualIdentities.create(containerObj);
     browser.test.assertTrue(!!ci, "We have an identity");
     const onCreateListenerResponse = await onCreatePromise;
     const cookieStoreId = ci.cookieStoreId;
     assertExpected(onCreateListenerResponse.contextualIdentity, Object.assign(containerObj, {cookieStoreId}));
 
     let onUpdatedPromise = createOneTimeListener("onUpdated");
-    let updateContainerObj = {name: "testing", color: "blue", icon: "thing"};
+    let updateContainerObj = {name: "testing", color: "blue", icon: "dollar"};
     ci = await browser.contextualIdentities.update(cookieStoreId, updateContainerObj);
     browser.test.assertTrue(!!ci, "We have an update identity");
     const onUpdatedListenerResponse = await onUpdatedPromise;
     assertExpected(onUpdatedListenerResponse.contextualIdentity, Object.assign(updateContainerObj, {cookieStoreId}));
 
     let onRemovePromise = createOneTimeListener("onRemoved");
     ci = await browser.contextualIdentities.remove(updateContainerObj.cookieStoreId);
     browser.test.assertTrue(!!ci, "We have an remove identity");
@@ -125,49 +131,64 @@ add_task(async function test_contextualI
     browser.test.assertEq(4, cis.length, "by default we should have 4 containers");
 
     cis = await browser.contextualIdentities.query({name: "Personal"});
     browser.test.assertEq(1, cis.length, "by default we should have 1 container called Personal");
 
     cis = await browser.contextualIdentities.query({name: "foobar"});
     browser.test.assertEq(0, cis.length, "by default we should have 0 container called foobar");
 
-    ci = await browser.contextualIdentities.create({name: "foobar", color: "red", icon: "icon"});
+    ci = await browser.contextualIdentities.create({name: "foobar", color: "red", icon: "gift"});
     browser.test.assertTrue(!!ci, "We have an identity");
     browser.test.assertEq("foobar", ci.name, "identity.name is correct");
     browser.test.assertEq("red", ci.color, "identity.color is correct");
-    browser.test.assertEq("icon", ci.icon, "identity.icon is correct");
+    browser.test.assertEq("gift", ci.icon, "identity.icon is correct");
     browser.test.assertTrue(!!ci.cookieStoreId, "identity.cookieStoreId is correct");
 
+    browser.test.assertRejects(
+      browser.contextualIdentities.create({name: "foobar", color: "red", icon: "firefox"}),
+      "Invalid icon firefox for container",
+      "Create container called with an invalid icon"
+    );
+
+    browser.test.assertRejects(
+      browser.contextualIdentities.create({name: "foobar", color: "firefox-orange", icon: "gift"}),
+      "Invalid color name firefox-orange for container",
+      "Create container called with an invalid color"
+    );
+
+    cis = await browser.contextualIdentities.query({});
+    browser.test.assertEq(5, cis.length, "we should still have have 5 containers");
+
     ci = await browser.contextualIdentities.get(ci.cookieStoreId);
     browser.test.assertTrue(!!ci, "We have an identity");
     browser.test.assertEq("foobar", ci.name, "identity.name is correct");
     browser.test.assertEq("red", ci.color, "identity.color is correct");
-    browser.test.assertEq("icon", ci.icon, "identity.icon is correct");
+    browser.test.assertEq("gift", ci.icon, "identity.icon is correct");
 
     cis = await browser.contextualIdentities.query({});
     browser.test.assertEq(5, cis.length, "now we have 5 identities");
 
-    ci = await browser.contextualIdentities.update(ci.cookieStoreId, {name: "barfoo", color: "blue", icon: "icon icon"});
+    ci = await browser.contextualIdentities.update(ci.cookieStoreId, {name: "barfoo", color: "blue", icon: "cart"});
     browser.test.assertTrue(!!ci, "We have an identity");
     browser.test.assertEq("barfoo", ci.name, "identity.name is correct");
     browser.test.assertEq("blue", ci.color, "identity.color is correct");
-    browser.test.assertEq("icon icon", ci.icon, "identity.icon is correct");
+    browser.test.assertEq("cart", ci.icon, "identity.icon is correct");
 
     ci = await browser.contextualIdentities.get(ci.cookieStoreId);
     browser.test.assertTrue(!!ci, "We have an identity");
     browser.test.assertEq("barfoo", ci.name, "identity.name is correct");
     browser.test.assertEq("blue", ci.color, "identity.color is correct");
-    browser.test.assertEq("icon icon", ci.icon, "identity.icon is correct");
+    browser.test.assertEq("cart", ci.icon, "identity.icon is correct");
 
     ci = await browser.contextualIdentities.remove(ci.cookieStoreId);
     browser.test.assertTrue(!!ci, "We have an identity");
     browser.test.assertEq("barfoo", ci.name, "identity.name is correct");
     browser.test.assertEq("blue", ci.color, "identity.color is correct");
-    browser.test.assertEq("icon icon", ci.icon, "identity.icon is correct");
+    browser.test.assertEq("cart", ci.icon, "identity.icon is correct");
 
     cis = await browser.contextualIdentities.query({});
     browser.test.assertEq(4, cis.length, "we are back to 4 identities");
 
     browser.test.notifyPass("contextualIdentities");
   }
   function makeExtension(id) {
     return ExtensionTestUtils.loadExtension({