toolkit/mozapps/extensions/LightweightThemeManager.jsm

branch
TOR_BUG_3246
changeset 7
129ffea94266
equal deleted inserted replaced
-1:000000000000 0:b3145a6cd32c
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5 "use strict";
6
7 this.EXPORTED_SYMBOLS = ["LightweightThemeManager"];
8
9 const Cc = Components.classes;
10 const Ci = Components.interfaces;
11
12 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
13 Components.utils.import("resource://gre/modules/AddonManager.jsm");
14 Components.utils.import("resource://gre/modules/Services.jsm");
15
16 const ID_SUFFIX = "@personas.mozilla.org";
17 const PREF_LWTHEME_TO_SELECT = "extensions.lwThemeToSelect";
18 const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin";
19 const PREF_EM_DSS_ENABLED = "extensions.dss.enabled";
20 const ADDON_TYPE = "theme";
21
22 const URI_EXTENSION_STRINGS = "chrome://mozapps/locale/extensions/extensions.properties";
23
24 const STRING_TYPE_NAME = "type.%ID%.name";
25
26 const DEFAULT_MAX_USED_THEMES_COUNT = 30;
27
28 const MAX_PREVIEW_SECONDS = 30;
29
30 const MANDATORY = ["id", "name", "headerURL"];
31 const OPTIONAL = ["footerURL", "textcolor", "accentcolor", "iconURL",
32 "previewURL", "author", "description", "homepageURL",
33 "updateURL", "version"];
34
35 const PERSIST_ENABLED = true;
36 const PERSIST_BYPASS_CACHE = false;
37 const PERSIST_FILES = {
38 headerURL: "lightweighttheme-header",
39 footerURL: "lightweighttheme-footer"
40 };
41
42 XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeImageOptimizer",
43 "resource://gre/modules/addons/LightweightThemeImageOptimizer.jsm");
44
45 this.__defineGetter__("_prefs", function prefsGetter() {
46 delete this._prefs;
47 return this._prefs = Services.prefs.getBranch("lightweightThemes.");
48 });
49
50 this.__defineGetter__("_maxUsedThemes", function maxUsedThemesGetter() {
51 delete this._maxUsedThemes;
52 try {
53 this._maxUsedThemes = _prefs.getIntPref("maxUsedThemes");
54 }
55 catch (e) {
56 this._maxUsedThemes = DEFAULT_MAX_USED_THEMES_COUNT;
57 }
58 return this._maxUsedThemes;
59 });
60
61 this.__defineSetter__("_maxUsedThemes", function maxUsedThemesSetter(aVal) {
62 delete this._maxUsedThemes;
63 return this._maxUsedThemes = aVal;
64 });
65
66 // Holds the ID of the theme being enabled or disabled while sending out the
67 // events so cached AddonWrapper instances can return correct values for
68 // permissions and pendingOperations
69 var _themeIDBeingEnabled = null;
70 var _themeIDBeingDisabled = null;
71
72 this.LightweightThemeManager = {
73 get usedThemes () {
74 try {
75 return JSON.parse(_prefs.getComplexValue("usedThemes",
76 Ci.nsISupportsString).data);
77 } catch (e) {
78 return [];
79 }
80 },
81
82 get currentTheme () {
83 try {
84 if (_prefs.getBoolPref("isThemeSelected"))
85 var data = this.usedThemes[0];
86 } catch (e) {}
87
88 return data || null;
89 },
90
91 get currentThemeForDisplay () {
92 var data = this.currentTheme;
93
94 if (data && PERSIST_ENABLED) {
95 for (let key in PERSIST_FILES) {
96 try {
97 if (data[key] && _prefs.getBoolPref("persisted." + key))
98 data[key] = _getLocalImageURI(PERSIST_FILES[key]).spec
99 + "?" + data.id + ";" + _version(data);
100 } catch (e) {}
101 }
102 }
103
104 return data;
105 },
106
107 set currentTheme (aData) {
108 return _setCurrentTheme(aData, false);
109 },
110
111 setLocalTheme: function LightweightThemeManager_setLocalTheme(aData) {
112 _setCurrentTheme(aData, true);
113 },
114
115 getUsedTheme: function LightweightThemeManager_getUsedTheme(aId) {
116 var usedThemes = this.usedThemes;
117 for (let usedTheme of usedThemes) {
118 if (usedTheme.id == aId)
119 return usedTheme;
120 }
121 return null;
122 },
123
124 forgetUsedTheme: function LightweightThemeManager_forgetUsedTheme(aId) {
125 let theme = this.getUsedTheme(aId);
126 if (!theme)
127 return;
128
129 let wrapper = new AddonWrapper(theme);
130 AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper, false);
131
132 var currentTheme = this.currentTheme;
133 if (currentTheme && currentTheme.id == aId) {
134 this.themeChanged(null);
135 AddonManagerPrivate.notifyAddonChanged(null, ADDON_TYPE, false);
136 }
137
138 _updateUsedThemes(_usedThemesExceptId(aId));
139 AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
140 },
141
142 previewTheme: function LightweightThemeManager_previewTheme(aData) {
143 if (!aData)
144 return;
145
146 let cancel = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
147 cancel.data = false;
148 Services.obs.notifyObservers(cancel, "lightweight-theme-preview-requested",
149 JSON.stringify(aData));
150 if (cancel.data)
151 return;
152
153 if (_previewTimer)
154 _previewTimer.cancel();
155 else
156 _previewTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
157 _previewTimer.initWithCallback(_previewTimerCallback,
158 MAX_PREVIEW_SECONDS * 1000,
159 _previewTimer.TYPE_ONE_SHOT);
160
161 _notifyWindows(aData);
162 },
163
164 resetPreview: function LightweightThemeManager_resetPreview() {
165 if (_previewTimer) {
166 _previewTimer.cancel();
167 _previewTimer = null;
168 _notifyWindows(this.currentThemeForDisplay);
169 }
170 },
171
172 parseTheme: function LightweightThemeManager_parseTheme(aString, aBaseURI) {
173 try {
174 return _sanitizeTheme(JSON.parse(aString), aBaseURI, false);
175 } catch (e) {
176 return null;
177 }
178 },
179
180 updateCurrentTheme: function LightweightThemeManager_updateCurrentTheme() {
181 try {
182 if (!_prefs.getBoolPref("update.enabled"))
183 return;
184 } catch (e) {
185 return;
186 }
187
188 var theme = this.currentTheme;
189 if (!theme || !theme.updateURL)
190 return;
191
192 var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
193 .createInstance(Ci.nsIXMLHttpRequest);
194
195 req.mozBackgroundRequest = true;
196 req.overrideMimeType("text/plain");
197 req.open("GET", theme.updateURL, true);
198 // Prevent the request from reading from the cache.
199 req.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
200 // Prevent the request from writing to the cache.
201 req.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
202
203 var self = this;
204 req.addEventListener("load", function loadEventListener() {
205 if (req.status != 200)
206 return;
207
208 let newData = self.parseTheme(req.responseText, theme.updateURL);
209 if (!newData ||
210 newData.id != theme.id ||
211 _version(newData) == _version(theme))
212 return;
213
214 var currentTheme = self.currentTheme;
215 if (currentTheme && currentTheme.id == theme.id)
216 self.currentTheme = newData;
217 }, false);
218
219 req.send(null);
220 },
221
222 /**
223 * Switches to a new lightweight theme.
224 *
225 * @param aData
226 * The lightweight theme to switch to
227 */
228 themeChanged: function LightweightThemeManager_themeChanged(aData) {
229 if (_previewTimer) {
230 _previewTimer.cancel();
231 _previewTimer = null;
232 }
233
234 if (aData) {
235 let usedThemes = _usedThemesExceptId(aData.id);
236 usedThemes.unshift(aData);
237 _updateUsedThemes(usedThemes);
238 if (PERSIST_ENABLED) {
239 LightweightThemeImageOptimizer.purge();
240 _persistImages(aData, function themeChanged_persistImages() {
241 _notifyWindows(this.currentThemeForDisplay);
242 }.bind(this));
243 }
244 }
245
246 _prefs.setBoolPref("isThemeSelected", aData != null);
247 _notifyWindows(aData);
248 Services.obs.notifyObservers(null, "lightweight-theme-changed", null);
249 },
250
251 /**
252 * Starts the Addons provider and enables the new lightweight theme if
253 * necessary.
254 */
255 startup: function LightweightThemeManager_startup() {
256 if (Services.prefs.prefHasUserValue(PREF_LWTHEME_TO_SELECT)) {
257 let id = Services.prefs.getCharPref(PREF_LWTHEME_TO_SELECT);
258 if (id)
259 this.themeChanged(this.getUsedTheme(id));
260 else
261 this.themeChanged(null);
262 Services.prefs.clearUserPref(PREF_LWTHEME_TO_SELECT);
263 }
264
265 _prefs.addObserver("", _prefObserver, false);
266 },
267
268 /**
269 * Shuts down the provider.
270 */
271 shutdown: function LightweightThemeManager_shutdown() {
272 _prefs.removeObserver("", _prefObserver);
273 },
274
275 /**
276 * Called when a new add-on has been enabled when only one add-on of that type
277 * can be enabled.
278 *
279 * @param aId
280 * The ID of the newly enabled add-on
281 * @param aType
282 * The type of the newly enabled add-on
283 * @param aPendingRestart
284 * true if the newly enabled add-on will only become enabled after a
285 * restart
286 */
287 addonChanged: function LightweightThemeManager_addonChanged(aId, aType, aPendingRestart) {
288 if (aType != ADDON_TYPE)
289 return;
290
291 let id = _getInternalID(aId);
292 let current = this.currentTheme;
293
294 try {
295 let next = Services.prefs.getCharPref(PREF_LWTHEME_TO_SELECT);
296 if (id == next && aPendingRestart)
297 return;
298
299 Services.prefs.clearUserPref(PREF_LWTHEME_TO_SELECT);
300 if (next) {
301 AddonManagerPrivate.callAddonListeners("onOperationCancelled",
302 new AddonWrapper(this.getUsedTheme(next)));
303 }
304 else {
305 if (id == current.id) {
306 AddonManagerPrivate.callAddonListeners("onOperationCancelled",
307 new AddonWrapper(current));
308 return;
309 }
310 }
311 }
312 catch (e) {
313 }
314
315 if (current) {
316 if (current.id == id)
317 return;
318 _themeIDBeingDisabled = current.id;
319 let wrapper = new AddonWrapper(current);
320 if (aPendingRestart) {
321 Services.prefs.setCharPref(PREF_LWTHEME_TO_SELECT, "");
322 AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, true);
323 }
324 else {
325 AddonManagerPrivate.callAddonListeners("onDisabling", wrapper, false);
326 this.themeChanged(null);
327 AddonManagerPrivate.callAddonListeners("onDisabled", wrapper);
328 }
329 _themeIDBeingDisabled = null;
330 }
331
332 if (id) {
333 let theme = this.getUsedTheme(id);
334 _themeIDBeingEnabled = id;
335 let wrapper = new AddonWrapper(theme);
336 if (aPendingRestart) {
337 AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, true);
338 Services.prefs.setCharPref(PREF_LWTHEME_TO_SELECT, id);
339
340 // Flush the preferences to disk so they survive any crash
341 Services.prefs.savePrefFile(null);
342 }
343 else {
344 AddonManagerPrivate.callAddonListeners("onEnabling", wrapper, false);
345 this.themeChanged(theme);
346 AddonManagerPrivate.callAddonListeners("onEnabled", wrapper);
347 }
348 _themeIDBeingEnabled = null;
349 }
350 },
351
352 /**
353 * Called to get an Addon with a particular ID.
354 *
355 * @param aId
356 * The ID of the add-on to retrieve
357 * @param aCallback
358 * A callback to pass the Addon to
359 */
360 getAddonByID: function LightweightThemeManager_getAddonByID(aId, aCallback) {
361 let id = _getInternalID(aId);
362 if (!id) {
363 aCallback(null);
364 return;
365 }
366
367 let theme = this.getUsedTheme(id);
368 if (!theme) {
369 aCallback(null);
370 return;
371 }
372
373 aCallback(new AddonWrapper(theme));
374 },
375
376 /**
377 * Called to get Addons of a particular type.
378 *
379 * @param aTypes
380 * An array of types to fetch. Can be null to get all types.
381 * @param aCallback
382 * A callback to pass an array of Addons to
383 */
384 getAddonsByTypes: function LightweightThemeManager_getAddonsByTypes(aTypes, aCallback) {
385 if (aTypes && aTypes.indexOf(ADDON_TYPE) == -1) {
386 aCallback([]);
387 return;
388 }
389
390 aCallback([new AddonWrapper(a) for each (a in this.usedThemes)]);
391 },
392 };
393
394 /**
395 * The AddonWrapper wraps lightweight theme to provide the data visible to
396 * consumers of the AddonManager API.
397 */
398 function AddonWrapper(aTheme) {
399 this.__defineGetter__("id", function AddonWrapper_idGetter() aTheme.id + ID_SUFFIX);
400 this.__defineGetter__("type", function AddonWrapper_typeGetter() ADDON_TYPE);
401 this.__defineGetter__("isActive", function AddonWrapper_isActiveGetter() {
402 let current = LightweightThemeManager.currentTheme;
403 if (current)
404 return aTheme.id == current.id;
405 return false;
406 });
407
408 this.__defineGetter__("name", function AddonWrapper_nameGetter() aTheme.name);
409 this.__defineGetter__("version", function AddonWrapper_versionGetter() {
410 return "version" in aTheme ? aTheme.version : "";
411 });
412
413 ["description", "homepageURL", "iconURL"].forEach(function(prop) {
414 this.__defineGetter__(prop, function AddonWrapper_optionalPropGetter() {
415 return prop in aTheme ? aTheme[prop] : null;
416 });
417 }, this);
418
419 ["installDate", "updateDate"].forEach(function(prop) {
420 this.__defineGetter__(prop, function AddonWrapper_datePropGetter() {
421 return prop in aTheme ? new Date(aTheme[prop]) : null;
422 });
423 }, this);
424
425 this.__defineGetter__("creator", function AddonWrapper_creatorGetter() {
426 return new AddonManagerPrivate.AddonAuthor(aTheme.author);
427 });
428
429 this.__defineGetter__("screenshots", function AddonWrapper_screenshotsGetter() {
430 let url = aTheme.previewURL;
431 return [new AddonManagerPrivate.AddonScreenshot(url)];
432 });
433
434 this.__defineGetter__("pendingOperations",
435 function AddonWrapper_pendingOperationsGetter() {
436 let pending = AddonManager.PENDING_NONE;
437 if (this.isActive == this.userDisabled)
438 pending |= this.isActive ? AddonManager.PENDING_DISABLE : AddonManager.PENDING_ENABLE;
439 return pending;
440 });
441
442 this.__defineGetter__("operationsRequiringRestart",
443 function AddonWrapper_operationsRequiringRestartGetter() {
444 // If a non-default theme is in use then a restart will be required to
445 // enable lightweight themes unless dynamic theme switching is enabled
446 if (Services.prefs.prefHasUserValue(PREF_GENERAL_SKINS_SELECTEDSKIN)) {
447 try {
448 if (Services.prefs.getBoolPref(PREF_EM_DSS_ENABLED))
449 return AddonManager.OP_NEEDS_RESTART_NONE;
450 }
451 catch (e) {
452 }
453 return AddonManager.OP_NEEDS_RESTART_ENABLE;
454 }
455
456 return AddonManager.OP_NEEDS_RESTART_NONE;
457 });
458
459 this.__defineGetter__("size", function AddonWrapper_sizeGetter() {
460 // The size changes depending on whether the theme is in use or not, this is
461 // probably not worth exposing.
462 return null;
463 });
464
465 this.__defineGetter__("permissions", function AddonWrapper_permissionsGetter() {
466 let permissions = AddonManager.PERM_CAN_UNINSTALL;
467 if (this.userDisabled)
468 permissions |= AddonManager.PERM_CAN_ENABLE;
469 else
470 permissions |= AddonManager.PERM_CAN_DISABLE;
471 return permissions;
472 });
473
474 this.__defineGetter__("userDisabled", function AddonWrapper_userDisabledGetter() {
475 if (_themeIDBeingEnabled == aTheme.id)
476 return false;
477 if (_themeIDBeingDisabled == aTheme.id)
478 return true;
479
480 try {
481 let toSelect = Services.prefs.getCharPref(PREF_LWTHEME_TO_SELECT);
482 return aTheme.id != toSelect;
483 }
484 catch (e) {
485 let current = LightweightThemeManager.currentTheme;
486 return !current || current.id != aTheme.id;
487 }
488 });
489
490 this.__defineSetter__("userDisabled", function AddonWrapper_userDisabledSetter(val) {
491 if (val == this.userDisabled)
492 return val;
493
494 if (val)
495 LightweightThemeManager.currentTheme = null;
496 else
497 LightweightThemeManager.currentTheme = aTheme;
498
499 return val;
500 });
501
502 this.uninstall = function AddonWrapper_uninstall() {
503 LightweightThemeManager.forgetUsedTheme(aTheme.id);
504 };
505
506 this.cancelUninstall = function AddonWrapper_cancelUninstall() {
507 throw new Error("Theme is not marked to be uninstalled");
508 };
509
510 this.findUpdates = function AddonWrapper_findUpdates(listener, reason, appVersion, platformVersion) {
511 AddonManagerPrivate.callNoUpdateListeners(this, listener, reason, appVersion, platformVersion);
512 };
513 }
514
515 AddonWrapper.prototype = {
516 // Lightweight themes are never disabled by the application
517 get appDisabled() {
518 return false;
519 },
520
521 // Lightweight themes are always compatible
522 get isCompatible() {
523 return true;
524 },
525
526 get isPlatformCompatible() {
527 return true;
528 },
529
530 get scope() {
531 return AddonManager.SCOPE_PROFILE;
532 },
533
534 get foreignInstall() {
535 return false;
536 },
537
538 // Lightweight themes are always compatible
539 isCompatibleWith: function AddonWrapper_isCompatibleWith(appVersion, platformVersion) {
540 return true;
541 },
542
543 // Lightweight themes are always securely updated
544 get providesUpdatesSecurely() {
545 return true;
546 },
547
548 // Lightweight themes are never blocklisted
549 get blocklistState() {
550 return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
551 }
552 };
553
554 /**
555 * Converts the ID used by the public AddonManager API to an lightweight theme
556 * ID.
557 *
558 * @param id
559 * The ID to be converted
560 *
561 * @return the lightweight theme ID or null if the ID was not for a lightweight
562 * theme.
563 */
564 function _getInternalID(id) {
565 if (!id)
566 return null;
567 let len = id.length - ID_SUFFIX.length;
568 if (len > 0 && id.substring(len) == ID_SUFFIX)
569 return id.substring(0, len);
570 return null;
571 }
572
573 function _setCurrentTheme(aData, aLocal) {
574 aData = _sanitizeTheme(aData, null, aLocal);
575
576 let needsRestart = (ADDON_TYPE == "theme") &&
577 Services.prefs.prefHasUserValue(PREF_GENERAL_SKINS_SELECTEDSKIN);
578
579 let cancel = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
580 cancel.data = false;
581 Services.obs.notifyObservers(cancel, "lightweight-theme-change-requested",
582 JSON.stringify(aData));
583
584 if (aData) {
585 let theme = LightweightThemeManager.getUsedTheme(aData.id);
586 let isInstall = !theme || theme.version != aData.version;
587 if (isInstall) {
588 aData.updateDate = Date.now();
589 if (theme && "installDate" in theme)
590 aData.installDate = theme.installDate;
591 else
592 aData.installDate = aData.updateDate;
593
594 var oldWrapper = theme ? new AddonWrapper(theme) : null;
595 var wrapper = new AddonWrapper(aData);
596 AddonManagerPrivate.callInstallListeners("onExternalInstall", null,
597 wrapper, oldWrapper, false);
598 AddonManagerPrivate.callAddonListeners("onInstalling", wrapper, false);
599 }
600
601 let current = LightweightThemeManager.currentTheme;
602 let usedThemes = _usedThemesExceptId(aData.id);
603 if (current && current.id != aData.id)
604 usedThemes.splice(1, 0, aData);
605 else
606 usedThemes.unshift(aData);
607 _updateUsedThemes(usedThemes);
608
609 if (isInstall)
610 AddonManagerPrivate.callAddonListeners("onInstalled", wrapper);
611 }
612
613 if (cancel.data)
614 return null;
615
616 AddonManagerPrivate.notifyAddonChanged(aData ? aData.id + ID_SUFFIX : null,
617 ADDON_TYPE, needsRestart);
618
619 return LightweightThemeManager.currentTheme;
620 }
621
622 function _sanitizeTheme(aData, aBaseURI, aLocal) {
623 if (!aData || typeof aData != "object")
624 return null;
625
626 var resourceProtocols = ["http", "https"];
627 if (aLocal)
628 resourceProtocols.push("file");
629 var resourceProtocolExp = new RegExp("^(" + resourceProtocols.join("|") + "):");
630
631 function sanitizeProperty(prop) {
632 if (!(prop in aData))
633 return null;
634 if (typeof aData[prop] != "string")
635 return null;
636 let val = aData[prop].trim();
637 if (!val)
638 return null;
639
640 if (!/URL$/.test(prop))
641 return val;
642
643 try {
644 val = _makeURI(val, aBaseURI ? _makeURI(aBaseURI) : null).spec;
645 if ((prop == "updateURL" ? /^https:/ : resourceProtocolExp).test(val))
646 return val;
647 return null;
648 }
649 catch (e) {
650 return null;
651 }
652 }
653
654 let result = {};
655 for (let mandatoryProperty of MANDATORY) {
656 let val = sanitizeProperty(mandatoryProperty);
657 if (!val)
658 throw Components.results.NS_ERROR_INVALID_ARG;
659 result[mandatoryProperty] = val;
660 }
661
662 for (let optionalProperty of OPTIONAL) {
663 let val = sanitizeProperty(optionalProperty);
664 if (!val)
665 continue;
666 result[optionalProperty] = val;
667 }
668
669 return result;
670 }
671
672 function _usedThemesExceptId(aId)
673 LightweightThemeManager.usedThemes.filter(
674 function usedThemesExceptId_filterID(t) "id" in t && t.id != aId);
675
676 function _version(aThemeData)
677 aThemeData.version || "";
678
679 function _makeURI(aURL, aBaseURI)
680 Services.io.newURI(aURL, null, aBaseURI);
681
682 function _updateUsedThemes(aList) {
683 // Send uninstall events for all themes that need to be removed.
684 while (aList.length > _maxUsedThemes) {
685 let wrapper = new AddonWrapper(aList[aList.length - 1]);
686 AddonManagerPrivate.callAddonListeners("onUninstalling", wrapper, false);
687 aList.pop();
688 AddonManagerPrivate.callAddonListeners("onUninstalled", wrapper);
689 }
690
691 var str = Cc["@mozilla.org/supports-string;1"]
692 .createInstance(Ci.nsISupportsString);
693 str.data = JSON.stringify(aList);
694 _prefs.setComplexValue("usedThemes", Ci.nsISupportsString, str);
695
696 Services.obs.notifyObservers(null, "lightweight-theme-list-changed", null);
697 }
698
699 function _notifyWindows(aThemeData) {
700 Services.obs.notifyObservers(null, "lightweight-theme-styling-update",
701 JSON.stringify(aThemeData));
702 }
703
704 var _previewTimer;
705 var _previewTimerCallback = {
706 notify: function _previewTimerCallback_notify() {
707 LightweightThemeManager.resetPreview();
708 }
709 };
710
711 /**
712 * Called when any of the lightweightThemes preferences are changed.
713 */
714 function _prefObserver(aSubject, aTopic, aData) {
715 switch (aData) {
716 case "maxUsedThemes":
717 try {
718 _maxUsedThemes = _prefs.getIntPref(aData);
719 }
720 catch (e) {
721 _maxUsedThemes = DEFAULT_MAX_USED_THEMES_COUNT;
722 }
723 // Update the theme list to remove any themes over the number we keep
724 _updateUsedThemes(LightweightThemeManager.usedThemes);
725 break;
726 }
727 }
728
729 function _persistImages(aData, aCallback) {
730 function onSuccess(key) function () {
731 let current = LightweightThemeManager.currentTheme;
732 if (current && current.id == aData.id) {
733 _prefs.setBoolPref("persisted." + key, true);
734 }
735 if (--numFilesToPersist == 0 && aCallback) {
736 aCallback();
737 }
738 };
739
740 let numFilesToPersist = 0;
741 for (let key in PERSIST_FILES) {
742 _prefs.setBoolPref("persisted." + key, false);
743 if (aData[key]) {
744 numFilesToPersist++;
745 _persistImage(aData[key], PERSIST_FILES[key], onSuccess(key));
746 }
747 }
748 }
749
750 function _getLocalImageURI(localFileName) {
751 var localFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
752 localFile.append(localFileName);
753 return Services.io.newFileURI(localFile);
754 }
755
756 function _persistImage(sourceURL, localFileName, successCallback) {
757 if (/^file:/.test(sourceURL))
758 return;
759
760 var targetURI = _getLocalImageURI(localFileName);
761 var sourceURI = _makeURI(sourceURL);
762
763 var persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"]
764 .createInstance(Ci.nsIWebBrowserPersist);
765
766 persist.persistFlags =
767 Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES |
768 Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION |
769 (PERSIST_BYPASS_CACHE ?
770 Ci.nsIWebBrowserPersist.PERSIST_FLAGS_BYPASS_CACHE :
771 Ci.nsIWebBrowserPersist.PERSIST_FLAGS_FROM_CACHE);
772
773 persist.progressListener = new _persistProgressListener(successCallback);
774
775 persist.saveURI(sourceURI, null, null, null, null, targetURI, null);
776 }
777
778 function _persistProgressListener(successCallback) {
779 this.onLocationChange = function persistProgressListener_onLocationChange() {};
780 this.onProgressChange = function persistProgressListener_onProgressChange() {};
781 this.onStatusChange = function persistProgressListener_onStatusChange() {};
782 this.onSecurityChange = function persistProgressListener_onSecurityChange() {};
783 this.onStateChange = function persistProgressListener_onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
784 if (aRequest &&
785 aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
786 aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
787 try {
788 if (aRequest.QueryInterface(Ci.nsIHttpChannel).requestSucceeded) {
789 // success
790 successCallback();
791 return;
792 }
793 } catch (e) { }
794 // failure
795 }
796 };
797 }
798
799 AddonManagerPrivate.registerProvider(LightweightThemeManager, [
800 new AddonManagerPrivate.AddonType("theme", URI_EXTENSION_STRINGS,
801 STRING_TYPE_NAME,
802 AddonManager.VIEW_TYPE_LIST, 5000)
803 ]);

mercurial