Bug 1330341 - Add support for dynamic updates r?mikedeboer draft
authorMatthew Wein <mwein@mozilla.com>
Wed, 25 Jan 2017 15:11:20 -0800
changeset 468342 b72a50699eba86249c801e60832ea46059f134ad
parent 468305 1fe66bd0efba89df59d2046e8c91418eb5ae10b8
child 543921 4e6eabee43268731ffd56bea711c46b7bb6a6eb3
push id43435
push usermwein@mozilla.com
push dateTue, 31 Jan 2017 03:52:41 +0000
reviewersmikedeboer
bugs1330341
milestone54.0a1
Bug 1330341 - Add support for dynamic updates r?mikedeboer MozReview-Commit-ID: 8wA7J1oX2t
browser/components/extensions/ext-theme.js
browser/components/extensions/schemas/theme.json
browser/components/extensions/test/browser/browser-common.ini
browser/components/extensions/test/browser/browser_ext_themes_dynamic_updates.js
browser/components/extensions/test/browser/browser_ext_themes_lwtsupport.js
--- a/browser/components/extensions/ext-theme.js
+++ b/browser/components/extensions/ext-theme.js
@@ -1,69 +1,127 @@
-/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
-/* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 Cu.import("resource://gre/modules/Services.jsm");
 
 XPCOMUtils.defineLazyModuleGetter(this, "Preferences",
                                   "resource://gre/modules/Preferences.jsm");
 
-let themeExtensions = new WeakSet();
+// WeakMap[Extension -> Theme]
+let themeMap = new WeakMap();
 
-/* eslint-disable mozilla/balanced-listeners */
-extensions.on("manifest_theme", (type, directive, extension, manifest) => {
-  let enabled = Preferences.get("extensions.webextensions.themes.enabled");
-
-  if (!enabled || !manifest || !manifest.theme) {
-    return;
+/** Class representing a theme. */
+class Theme {
+  /**
+   * Creates a theme instance.
+   */
+  constructor() {
+    // A dictionary of light weight theme styles.
+    this.lwtStyles = {};
   }
-  // Apply theme only if themes are enabled.
-  let lwtStyles = {footerURL: ""};
-  if (manifest.theme.colors) {
-    let colors = manifest.theme.colors;
-    for (let color of Object.getOwnPropertyNames(colors)) {
-      let val = colors[color];
-      // Since values are optional, they may be `null`.
-      if (val === null) {
-        continue;
-      }
+
+  /**
+   * Loads a theme by reading the properties from the extension's manifest.
+   * This method will override any currently applied theme.
+   *
+   * @param {Object} details Theme part of the manifest. Supported
+   *   properties can be found in the schema under ThemeType.
+   */
+  load(details) {
+    if (details.colors) {
+      this.loadColors(details.colors);
+    }
+
+    if (details.images) {
+      this.loadImages(details.images);
+    }
 
-      if (color == "accentcolor") {
-        lwtStyles.accentcolor = val;
-        continue;
-      }
-      if (color == "textcolor") {
-        lwtStyles.textcolor = val;
-      }
+    // Lightweight themes require all properties to be defined.
+    if (this.lwtStyles.headerURL &&
+        this.lwtStyles.accentcolor &&
+        this.lwtStyles.textcolor) {
+      Services.obs.notifyObservers(null,
+        "lightweight-theme-styling-update",
+        JSON.stringify(this.lwtStyles));
+    }
+  }
+
+  /**
+   * Helper method for loading colors found in the extension's manifest.
+   *
+   * @param {Object} colors Dictionary mapping color properties to values.
+   */
+  loadColors(colors) {
+    let {accentcolor, textcolor} = colors;
+
+    if (accentcolor) {
+      this.lwtStyles.accentcolor = accentcolor;
+    }
+
+    if (textcolor) {
+      this.lwtStyles.textcolor = textcolor;
     }
   }
 
-  if (manifest.theme.images) {
-    let images = manifest.theme.images;
-    for (let image of Object.getOwnPropertyNames(images)) {
-      let val = images[image];
-      if (val === null) {
-        continue;
-      }
+  /**
+   * Helper method for loading images found in the extension's manifest.
+   *
+   * @param {Object} images Dictionary mapping image properties to values.
+   */
+  loadImages(images) {
+    let {headerURL} = images;
 
-      if (image == "headerURL") {
-        lwtStyles.headerURL = val;
-      }
+    if (headerURL) {
+      this.lwtStyles.headerURL = headerURL;
     }
   }
 
-  if (lwtStyles.headerURL &&
-      lwtStyles.accentcolor &&
-      lwtStyles.textcolor) {
-    themeExtensions.add(extension);
+  /**
+   * Unloads the currently applied theme.
+   */
+  unload() {
     Services.obs.notifyObservers(null,
       "lightweight-theme-styling-update",
-      JSON.stringify(lwtStyles));
+      null);
   }
+}
+
+/* eslint-disable mozilla/balanced-listeners */
+extensions.on("manifest_theme", (type, directive, extension, manifest) => {
+  if (!Preferences.get("extensions.webextensions.themes.enabled")) {
+    // Return early if themes are disabled.
+    return;
+  }
+
+  let theme = new Theme();
+  theme.load(manifest.theme);
+  themeMap.set(extension, theme);
 });
 
 extensions.on("shutdown", (type, extension) => {
-  if (themeExtensions.has(extension)) {
-    Services.obs.notifyObservers(null, "lightweight-theme-styling-update", null);
+  let theme = themeMap.get(extension);
+
+  // We won't have a theme if theme's aren't enabled.
+  if (!theme) {
+    return;
   }
+
+  theme.unload();
 });
 /* eslint-enable mozilla/balanced-listeners */
+
+extensions.registerSchemaAPI("theme", "addon_parent", context => {
+  let {extension} = context;
+  return {
+    theme: {
+      update(details) {
+        let theme = themeMap.get(extension);
+
+        // We won't have a theme if theme's aren't enabled.
+        if (!theme) {
+          return;
+        }
+
+        theme.load(details);
+      },
+    },
+  };
+});
--- a/browser/components/extensions/schemas/theme.json
+++ b/browser/components/extensions/schemas/theme.json
@@ -41,10 +41,30 @@
         "properties": {
           "theme": {
             "optional": true,
             "$ref": "ThemeType"
           }
         }
       }
     ]
+  },
+  {
+    "namespace": "theme",
+    "description": "The theme API allows customizing of visual elements of the browser.",
+    "permissions": ["manifest:theme"],
+    "functions": [
+      {
+        "name": "update",
+        "type": "function",
+        "async": true,
+        "description": "Make complete or partial updates to the theme. Resolves when the update has completed.",
+        "parameters": [
+          {
+            "name": "details",
+            "$ref": "manifest.ThemeType",
+            "description": "The properties of the theme to update."
+          }
+        ]
+      }
+    ]
   }
 ]
--- a/browser/components/extensions/test/browser/browser-common.ini
+++ b/browser/components/extensions/test/browser/browser-common.ini
@@ -102,16 +102,17 @@ support-files =
 [browser_ext_tabs_query.js]
 [browser_ext_tabs_reload.js]
 [browser_ext_tabs_reload_bypass_cache.js]
 [browser_ext_tabs_sendMessage.js]
 [browser_ext_tabs_cookieStoreId.js]
 [browser_ext_tabs_update.js]
 [browser_ext_tabs_zoom.js]
 [browser_ext_tabs_update_url.js]
+[browser_ext_themes_dynamic_updates.js]
 [browser_ext_themes_lwtsupport.js]
 [browser_ext_topwindowid.js]
 [browser_ext_url_overrides.js]
 [browser_ext_webRequest.js]
 [browser_ext_webNavigation_frameId0.js]
 [browser_ext_webNavigation_getFrames.js]
 [browser_ext_webNavigation_urlbar_transitions.js]
 [browser_ext_windows.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_themes_dynamic_updates.js
@@ -0,0 +1,87 @@
+"use strict";
+
+// PNG image data for a simple red dot.
+const BACKGROUND_1 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+const ACCENT_COLOR_1 = "#a14040";
+const TEXT_COLOR_1 = "#fac96e";
+
+// PNG image data for the Mozilla dino head.
+const BACKGROUND_2 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==";
+const ACCENT_COLOR_2 = "#03fe03";
+const TEXT_COLOR_2 = "#0ef325";
+
+function hexToRGB(hex) {
+  hex = parseInt((hex.indexOf("#") > -1 ? hex.substring(1) : hex), 16);
+  return [hex >> 16, (hex & 0x00FF00) >> 8, (hex & 0x0000FF)];
+}
+
+function validateTheme(backgroundImage, accentColor, textColor) {
+  let docEl = window.document.documentElement;
+  let style = window.getComputedStyle(docEl);
+
+  Assert.ok(docEl.hasAttribute("lwtheme"), "LWT attribute should be set");
+  Assert.equal(docEl.getAttribute("lwthemetextcolor"), "bright",
+    "LWT text color attribute should be set");
+
+  Assert.equal(style.backgroundImage, 'url("' + backgroundImage.replace(/"/g, '\\"') + '")',
+    "Expected correct background image");
+  Assert.equal(style.backgroundColor, "rgb(" + hexToRGB(accentColor).join(", ") + ")",
+    "Expected correct accent color");
+  Assert.equal(style.color, "rgb(" + hexToRGB(textColor).join(", ") + ")",
+    "Expected correct text color");
+}
+
+add_task(function* setup() {
+  yield SpecialPowers.pushPrefEnv({
+    set: [["extensions.webextensions.themes.enabled", true]],
+  });
+});
+
+add_task(function* test_dynamic_theme_updates() {
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: {
+      "theme": {
+        "images": {
+          "headerURL": BACKGROUND_1,
+        },
+        "colors": {
+          "accentcolor": ACCENT_COLOR_1,
+          "textcolor": TEXT_COLOR_1,
+        },
+      },
+    },
+    background() {
+      browser.test.onMessage.addListener((msg, details) => {
+        if (msg != "update-theme") {
+          browser.test.fail("expected 'update-theme' message");
+        }
+
+        browser.theme.update(details);
+        browser.test.sendMessage("theme-updated");
+      });
+    },
+  });
+
+  yield extension.startup();
+
+  validateTheme(BACKGROUND_1, ACCENT_COLOR_1, TEXT_COLOR_1);
+
+  extension.sendMessage("update-theme", {
+    "images": {
+      "headerURL": BACKGROUND_2,
+    },
+    "colors": {
+      "accentcolor": ACCENT_COLOR_2,
+      "textcolor": TEXT_COLOR_2,
+    },
+  });
+
+  yield extension.awaitMessage("theme-updated");
+
+  validateTheme(BACKGROUND_2, ACCENT_COLOR_2, TEXT_COLOR_2);
+
+  yield extension.unload();
+
+  let docEl = window.document.documentElement;
+  Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set");
+});
--- a/browser/components/extensions/test/browser/browser_ext_themes_lwtsupport.js
+++ b/browser/components/extensions/test/browser/browser_ext_themes_lwtsupport.js
@@ -1,87 +1,87 @@
 "use strict";
 
-const kBackground = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
-const kAccentColor = "#a14040";
-const kTextColor = "#fac96e";
+const BACKGROUND = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==";
+const ACCENT_COLOR = "#a14040";
+const TEXT_COLOR = "#fac96e";
 
 function hexToRGB(hex) {
   hex = parseInt((hex.indexOf("#") > -1 ? hex.substring(1) : hex), 16);
   return [hex >> 16, (hex & 0x00FF00) >> 8, (hex & 0x0000FF)];
 }
 
 add_task(function* setup() {
   yield SpecialPowers.pushPrefEnv({
     set: [["extensions.webextensions.themes.enabled", true]],
   });
 });
 
-add_task(function* testSupportLWTProperties() {
+add_task(function* test_support_LWT_properties() {
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "theme": {
         "images": {
-          "headerURL": kBackground,
+          "headerURL": BACKGROUND,
         },
         "colors": {
-          "accentcolor": kAccentColor,
-          "textcolor": kTextColor,
+          "accentcolor": ACCENT_COLOR,
+          "textcolor": TEXT_COLOR,
         },
       },
     },
   });
 
   yield extension.startup();
 
   let docEl = window.document.documentElement;
   let style = window.getComputedStyle(docEl);
 
   Assert.ok(docEl.hasAttribute("lwtheme"), "LWT attribute should be set");
   Assert.equal(docEl.getAttribute("lwthemetextcolor"), "bright",
     "LWT text color attribute should be set");
 
-  Assert.equal(style.backgroundImage, 'url("' + kBackground.replace(/"/g, '\\"') + '")',
+  Assert.equal(style.backgroundImage, 'url("' + BACKGROUND.replace(/"/g, '\\"') + '")',
     "Expected background image");
-  Assert.equal(style.backgroundColor, "rgb(" + hexToRGB(kAccentColor).join(", ") + ")",
+  Assert.equal(style.backgroundColor, "rgb(" + hexToRGB(ACCENT_COLOR).join(", ") + ")",
     "Expected correct background color");
-  Assert.equal(style.color, "rgb(" + hexToRGB(kTextColor).join(", ") + ")",
+  Assert.equal(style.color, "rgb(" + hexToRGB(TEXT_COLOR).join(", ") + ")",
     "Expected correct text color");
 
   yield extension.unload();
 
   Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set");
 });
 
-add_task(function* testLWTRequiresAllPropertiesDefinedImageOnly() {
+add_task(function* test_LWT_requires_all_properties_defined_image_only() {
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "theme": {
         "images": {
-          "headerURL": kBackground,
+          "headerURL": BACKGROUND,
         },
       },
     },
   });
 
   yield extension.startup();
 
   let docEl = window.document.documentElement;
   Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set");
   yield extension.unload();
   Assert.ok(!docEl.hasAttribute("lwtheme"), "LWT attribute should not be set");
 });
 
-add_task(function* testLWTRequiresAllPropertiesDefinedColorsOnly() {
+add_task(function* test_LWT_requires_all_properties_defined_colors_only() {
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "theme": {
         "colors": {
-          "accentcolor": kAccentColor,
-          "textcolor": kTextColor,
+          "accentcolor": ACCENT_COLOR,
+          "textcolor": TEXT_COLOR,
         },
       },
     },
   });
 
   yield extension.startup();
 
   let docEl = window.document.documentElement;