From dc25d2049db9572756acb2aa9e5e2d069c6975a0 Mon Sep 17 00:00:00 2001 From: Dave Townsend Date: Thu, 29 Apr 2010 13:11:23 -0700 Subject: [PATCH] Bug 553169: Implement an AddonProvider for lightweight themes. r=robstrong --- toolkit/mozapps/extensions/AddonManager.jsm | 2 + .../extensions/LightweightThemeManager.jsm | 348 ++++++++++++++++-- 2 files changed, 328 insertions(+), 22 deletions(-) diff --git a/toolkit/mozapps/extensions/AddonManager.jsm b/toolkit/mozapps/extensions/AddonManager.jsm index d535504db397..51e57394eafa 100644 --- a/toolkit/mozapps/extensions/AddonManager.jsm +++ b/toolkit/mozapps/extensions/AddonManager.jsm @@ -795,6 +795,8 @@ var AddonManager = { UPDATE_WHEN_ADDON_INSTALLED: 17, // Constants for operations in Addon.pendingOperations + // Indicates that the Addon has no pending operations. + PENDING_NONE: 0, // Indicates that the Addon will be enabled after the application restarts. PENDING_ENABLE: 1, // Indicates that the Addon will be disabled after the application restarts. diff --git a/toolkit/mozapps/extensions/LightweightThemeManager.jsm b/toolkit/mozapps/extensions/LightweightThemeManager.jsm index 3dafd3e05e10..0742ca12c820 100644 --- a/toolkit/mozapps/extensions/LightweightThemeManager.jsm +++ b/toolkit/mozapps/extensions/LightweightThemeManager.jsm @@ -39,6 +39,14 @@ var EXPORTED_SYMBOLS = ["LightweightThemeManager"]; const Cc = Components.classes; const Ci = Components.interfaces; +Components.utils.import("resource://gre/modules/AddonManager.jsm"); +Components.utils.import("resource://gre/modules/Services.jsm"); + +const ID_SUFFIX = "@personas.mozilla.org"; +const PREF_LWTHEME_TO_SELECT = "extensions.lwThemeToSelect"; +const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin"; +const ADDON_TYPE = "theme"; + const MAX_USED_THEMES_COUNT = 8; const MAX_PREVIEW_SECONDS = 30; @@ -127,11 +135,21 @@ var LightweightThemeManager = { }, forgetUsedTheme: function (aId) { + let theme = this.getUsedTheme(aId); + if (!theme) + return; + + let wrapper = new AddonWrapper(theme); + AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper, false); + var currentTheme = this.currentTheme; - if (currentTheme && currentTheme.id == aId) - this.currentTheme = null; + if (currentTheme && currentTheme.id == aId) { + _prefs.setBoolPref("isThemeSelected", false); + AddonManagerPrivate.notifyAddonChanged(null, ADDON_TYPE, false); + } _updateUsedThemes(_usedThemesExceptId(aId)); + AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper); }, previewTheme: function (aData) { @@ -208,42 +226,321 @@ var LightweightThemeManager = { }; req.send(null); + }, + + /** + * Switches to a new lightweight theme. + * + * @param aData + * The lightweight theme to switch to + */ + themeChanged: function(aData) { + if (_previewTimer) { + _previewTimer.cancel(); + _previewTimer = null; + } + + if (aData) { + let usedThemes = _usedThemesExceptId(aData.id); + usedThemes.unshift(aData); + _updateUsedThemes(usedThemes); + if (PERSIST_ENABLED) + _persistImages(aData); + } + + _prefs.setBoolPref("isThemeSelected", aData != null); + _notifyWindows(aData); + _observerService.notifyObservers(null, "lightweight-theme-changed", null); + }, + + /** + * Starts the Addons provider and enables the new lightweight theme if + * necessary. + */ + startup: function() { + if (Services.prefs.prefHasUserValue(PREF_LWTHEME_TO_SELECT)) { + let id = Services.prefs.getCharPref(PREF_LWTHEME_TO_SELECT); + if (id) + this.themeChanged(this.getUsedTheme(id)); + else + this.themeChanged(null); + Services.prefs.clearUserPref(PREF_LWTHEME_TO_SELECT); + } + }, + + /** + * Called when a new add-on has been enabled when only one add-on of that type + * can be enabled. + * + * @param aId + * The ID of the newly enabled add-on + * @param aType + * The type of the newly enabled add-on + * @param aPendingRestart + * true if the newly enabled add-on will only become enabled after a + * restart + */ + addonChanged: function(aId, aType, aPendingRestart) { + if (aType != ADDON_TYPE) + return; + + let id = _getInternalID(aId); + let current = this.currentTheme; + + try { + let next = Services.prefs.getCharPref(PREF_LWTHEME_TO_SELECT); + if (id == next && aPendingRestart) + return; + + Services.prefs.clearUserPref(PREF_LWTHEME_TO_SELECT); + if (next) { + AddonManagerPrivate.callAddonListeners("onOperationCancelled", + new AddonWrapper(this.getUsedTheme(next))); + } + else { + if (id == current.id) { + AddonManagerPrivate.callAddonListeners("onOperationCancelled", + new AddonWrapper(current)); + return; + } + } + } + catch (e) { + } + + if (current) { + if (current.id == id) + return; + let wrapper = new AddonWrapper(current); + if (aPendingRestart) { + Services.prefs.setCharPref(PREF_LWTHEME_TO_SELECT, ""); + AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, true); + } + else { + AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, false); + this.themeChanged(null); + AddonManagerPrivate.callAddonListeners("onDisabled", wrapper); + } + } + + if (id) { + let theme = this.getUsedTheme(id); + let wrapper = new AddonWrapper(theme, true); + if (aPendingRestart) { + AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, true); + Services.prefs.setCharPref(PREF_LWTHEME_TO_SELECT, id); + } + else { + AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, false); + this.themeChanged(theme); + AddonManagerPrivate.callAddonListeners("onEnabled", wrapper); + } + } + }, + + /** + * Called to get an Addon with a particular ID. + * + * @param aId + * The ID of the add-on to retrieve + * @param aCallback + * A callback to pass the Addon to + */ + getAddon: function(aId, aCallback) { + let id = _getInternalID(aId); + if (!id) { + aCallback(null); + return; + } + + let theme = this.getUsedTheme(id); + if (!theme) { + aCallback(null); + return; + } + + aCallback(new AddonWrapper(theme)); + }, + + /** + * Called to get Addons of a particular type. + * + * @param aTypes + * An array of types to fetch. Can be null to get all types. + * @param aCallback + * A callback to pass an array of Addons to + */ + getAddonsByTypes: function(aTypes, aCallback) { + if (aTypes && aTypes.indexOf(ADDON_TYPE) == -1) { + aCallback([]); + return; + } + + aCallback([new AddonWrapper(a) for each (a in this.usedThemes)]); + }, +}; + +/** + * The AddonWrapper wraps lightweight theme to provide the data visible to + * consumers of the AddonManager API. + */ +function AddonWrapper(aTheme, aBeingEnabled) { + this.__defineGetter__("id", function() aTheme.id + ID_SUFFIX); + this.__defineGetter__("type", function() ADDON_TYPE); + this.__defineGetter__("isActive", function() { + let current = LightweightThemeManager.currentTheme; + if (current) + return aTheme.id == current.id; + return false; + }); + + ["name", "version", "description", "homepageURL", "iconURL"].forEach(function(prop) { + this.__defineGetter__(prop, function() aTheme[prop]); + }, this); + this.__defineGetter__("creator", function() aTheme.author); + this.__defineGetter__("screenshots", function() [aTheme.previewURL]); + + this.__defineGetter__("pendingOperations", function() { + let pending = AddonManager.PENDING_NONE; + if (this.isActive == this.userDisabled) + pending |= this.isActive ? AddonManager.PENDING_DISABLE : AddonManager.PENDING_ENABLE; + return pending; + }); + + this.__defineGetter__("permissions", function() { + let permissions = AddonManager.PERM_CAN_UNINSTALL; + if (this.userDisabled) + permissions |= AddonManager.PERM_CAN_ENABLE; + return permissions; + }); + + this.__defineGetter__("userDisabled", function() { + if (aBeingEnabled) + return false; + + try { + let toSelect = Services.prefs.getCharPref(PREF_LWTHEME_TO_SELECT); + return aTheme.id != toSelect; + } + catch (e) { + let current = LightweightThemeManager.currentTheme; + return !current || current.id != aTheme.id; + } + }); + + this.__defineSetter__("userDisabled", function(val) { + if (val == this.userDisabled) + return val; + + if (val) + throw new Error("Cannot disable the active theme"); + + LightweightThemeManager.currentTheme = aTheme; + return val; + }); + + this.uninstall = function() { + LightweightThemeManager.forgetUsedTheme(aTheme.id); + }; + + this.cancelUninstall = function() { + throw new Error("Theme is not marked to be uninstalled"); + }; + + this.findUpdates = function(listener, reason, appVersion, platformVersion) { + if ("onNoCompatibilityUpdateAvailable" in listener) + listener.onNoCompatibilityUpdateAvailable(this); + if ("onNoUpdateAvailable" in listener) + listener.onNoUpdateAvailable(this); + if ("onUpdateFinished" in listener) + listener.onUpdateFinished(this); + }; +} + +AddonWrapper.prototype = { + // Lightweight themes are never disabled by the application + get appDisabled() { + return false; + }, + + // Lightweight themes are always compatible + get isCompatible() { + return true; + }, + + // Lightweight themes are always compatible + isCompatibleWith: function(appVersion, platformVersion) { + return true; + }, + + // Lightweight themes are always securely updated + get providesUpdatesSecurely() { + return true; + }, + + // Lightweight themes are never blocklisted + get blocklistState() { + return Ci.nsIBlocklistService.STATE_NOT_BLOCKED; } }; +/** + * Converts the ID used by the public AddonManager API to an lightweight theme + * ID. + * + * @param id + * The ID to be converted + * + * @return the lightweight theme ID or null if the ID was not for a lightweight + * theme. + */ +function _getInternalID(id) { + if (!id) + return null; + let len = id.length - ID_SUFFIX.length; + if (len > 0 && id.substring(len) == ID_SUFFIX) + return id.substring(0, len); + return null; +} + function _setCurrentTheme(aData, aLocal) { aData = _sanitizeTheme(aData, null, aLocal); + let needsRestart = (ADDON_TYPE == "theme") && + Services.prefs.prefHasUserValue(PREF_GENERAL_SKINS_SELECTEDSKIN); + let cancel = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); cancel.data = false; _observerService.notifyObservers(cancel, "lightweight-theme-change-requested", JSON.stringify(aData)); if (aData) { - let usedThemes = _usedThemesExceptId(aData.id); - if (cancel.data && _prefs.getBoolPref("isThemeSelected")) - usedThemes.splice(1, 0, aData); - else - usedThemes.unshift(aData); - _updateUsedThemes(usedThemes); + let theme = LightweightThemeManager.getUsedTheme(aData.id); + if (!theme) { + var wrapper = new AddonWrapper(aData); + AddonManagerPrivate.callAddonListeners("onInstalling", wrapper, false); + } + let current = LightweightThemeManager.currentTheme; + if (!current || current.id != aData.id) { + let usedThemes = _usedThemesExceptId(aData.id); + if (current) + usedThemes.splice(1, 0, aData); + else + usedThemes.unshift(aData); + _updateUsedThemes(usedThemes); + } + + if (!theme) + AddonManagerPrivate.callAddonListeners("onInstalled", wrapper); } if (cancel.data) return null; - if (_previewTimer) { - _previewTimer.cancel(); - _previewTimer = null; - } + AddonManagerPrivate.notifyAddonChanged(aData ? aData.id + ID_SUFFIX : null, + ADDON_TYPE, needsRestart); - _prefs.setBoolPref("isThemeSelected", aData != null); - _notifyWindows(aData); - _observerService.notifyObservers(null, "lightweight-theme-changed", null); - - if (PERSIST_ENABLED && aData) - _persistImages(aData); - - return aData; + return LightweightThemeManager.currentTheme; } function _sanitizeTheme(aData, aBaseURI, aLocal) { @@ -306,8 +603,13 @@ function _makeURI(aURL, aBaseURI) _ioService.newURI(aURL, null, aBaseURI); function _updateUsedThemes(aList) { - if (aList.length > MAX_USED_THEMES_COUNT) - aList.length = MAX_USED_THEMES_COUNT; + // Send uninstall events for all themes that need to be removed. + while (aList.length > MAX_USED_THEMES_COUNT) { + let wrapper = new AddonWrapper(aList[aList.length - 1]); + AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper, false); + aList.pop(); + AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper); + } var str = Cc["@mozilla.org/supports-string;1"] .createInstance(Ci.nsISupportsString); @@ -393,3 +695,5 @@ function _persistProgressListener(successCallback) { } }; } + +AddonManagerPrivate.registerProvider(LightweightThemeManager);