sbFeathersManager.js
Go to the documentation of this file.
1 /*
2  *=BEGIN SONGBIRD GPL
3  *
4  * This file is part of the Songbird web player.
5  *
6  * Copyright(c) 2005-2010 POTI, Inc.
7  * http://www.songbirdnest.com
8  *
9  * This file may be licensed under the terms of of the
10  * GNU General Public License Version 2 (the ``GPL'').
11  *
12  * Software distributed under the License is distributed
13  * on an ``AS IS'' basis, WITHOUT WARRANTY OF ANY KIND, either
14  * express or implied. See the GPL for the specific language
15  * governing rights and limitations.
16  *
17  * You should have received a copy of the GPL along with this
18  * program. If not, go to http://www.gnu.org/licenses/gpl.html
19  * or write to the Free Software Foundation, Inc.,
20  * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
21  *
22  *=END SONGBIRD GPL
23  */
24 
31 //
32 // TODO:
33 // * Explore skin/layout versioning issues?
34 //
35 
36 Components.utils.import("resource://app/jsmodules/ObserverUtils.jsm");
37 Components.utils.import("resource://app/jsmodules/StringUtils.jsm");
38 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
39 
40 const Ci = Components.interfaces;
41 const Cc = Components.classes;
42 const Cr = Components.results;
43 const Cu = Components.utils;
44 
45 const CONTRACTID = "@songbirdnest.com/songbird/feathersmanager;1";
46 const CLASSNAME = "Songbird Feathers Manager Service Interface";
47 const CID = Components.ID("{99f24350-a67f-11db-befa-0800200c9a66}");
48 const IID = Ci.sbIFeathersManager;
49 
50 
51 const RDFURI_ADDON_ROOT = "urn:songbird:addon:root"
52 const PREFIX_NS_SONGBIRD = "http://www.songbirdnest.com/2007/addon-metadata-rdf#";
53 
54 const CHROME_PREFIX = "chrome://"
55 
56 // This DataRemote is required to indicate to the feather manager
57 // that it is currently running in test mode.
58 const DATAREMOTE_TESTMODE = "__testmode__";
59 
60 //
61 // Default feather/layout/skin preference prefs.
62 //
63 // * PREF_DEFAULT_MAIN_LAYOUT
64 // - Default main layout to start with. This is typically the "main player"
65 // layout. This layout will be used when the player window is opened for
66 // the first time.
67 //
68 // * PREF_DEFAULT_SECONDARY_LAYOUT
69 // - The default secondary layout. This layout is typically an alternative
70 // view to the main layout defined in |PREF_DEFAULT_MAIN_LAYOUT| - such
71 // as a "mini-player" view.
72 // - NOTE: This is an optional pref.
73 //
74 // * PREF_DEFAULT_SKIN_LOCALNAME
75 // - The local name of the default feather. This is defined in the install.rdf
76 // of the shipped feather, in the |<songbird:internalName/>| tag.
77 //
78 // * PREF_DEFAULT_FEATHER_ID
79 // - The ID of the default feathers extension. This is defined in the
80 // install.rdf of the shipped feather, in the <em:id/> tag.
81 //
82 //
83 // NOTE: Changes to the default layout/skin/feather will be automatically picked up
84 // in the feathers unit test.
85 //
86 const PREF_DEFAULT_MAIN_LAYOUT = "songbird.feathers.default_main_layout";
87 const PREF_DEFAULT_SECONDARY_LAYOUT = "songbird.feathers.default_secondary_layout";
88 const PREF_DEFAULT_SKIN_INTERNALNAME = "songbird.feathers.default_skin_internalname";
89 const PREF_DEFAULT_FEATHER_ID = "songbird.feathers.default_feather_id";
90 
91 // Pref to ensure sanity tests are run the first time the feathers manager
92 // has started on a profile.
93 const PREF_FEATHERS_MANAGER_HAS_STARTED = "songbird.feathersmanager.hasStarted";
94 
95 const WINDOWTYPE_SONGBIRD_PLAYER = "Songbird:Main";
96 const WINDOWTYPE_SONGBIRD_CORE = "Songbird:Core";
97 
98 Cu.import("resource://app/jsmodules/RDFHelper.jsm");
99 Cu.import("resource://app/jsmodules/SBDataRemoteUtils.jsm");
100 Cu.import("resource://app/jsmodules/ArrayConverter.jsm");
101 Cu.import("resource://app/jsmodules/PlatformUtils.jsm");
102 
106 function SkinDescription() {};
107 SkinDescription.prototype = {
108  // TODO Expand?
109  requiredProperties: [ "internalName" ],
110  optionalProperties: [ "name" ],
111  QueryInterface: function(iid) {
112  if (!iid.equals(Ci.sbISkinDescription))
113  throw Components.results.NS_ERROR_NO_INTERFACE;
114  return this;
115  }
116 };
117 
121 function LayoutDescription() {};
122 LayoutDescription.prototype = {
123  // TODO Expand?
124  requiredProperties: [ "name", "url" ],
125  optionalProperties: [ ],
126  QueryInterface: function(iid) {
127  if (!iid.equals(Ci.sbILayoutDescription))
128  throw Components.results.NS_ERROR_NO_INTERFACE;
129  return this;
130  }
131 };
132 
144 LayoutDescription.verify = SkinDescription.verify = function( description )
145 {
146  for (var i = 0; i < this.prototype.requiredProperties.length; i++) {
147  var property = this.prototype.requiredProperties[i];
148  if (! (typeof(description[property]) == 'string'
149  && description[property].length > 0))
150  {
151  throw("Invalid description. '" + property + "' is a required property.");
152  }
153  }
154 }
155 
156 
162 function AddonMetadataReader() {};
163 
164 AddonMetadataReader.prototype = {
165  _manager: null,
166 
170  loadMetadata: function(manager) {
171  //debug("AddonMetadataReader: loadMetadata\n");
172  this._manager = manager;
173 
174  var addons = RDFHelper.help(
175  "rdf:addon-metadata",
176  "urn:songbird:addon:root",
177  RDFHelper.DEFAULT_RDF_NAMESPACES
178  );
179 
180  for (var i = 0; i < addons.length; i++) {
181  // first a little workaround to for backwards compatibility
182  // with the now obsolete <feathers> element
183  // TODO: remove this when we stop supporting 0.4 feathers
184  var feathersHub = addons[i];
185  if (feathersHub.feathers) {
186  Components.utils.reportError("Feathers Metadata Reader: The <feathers/> element in " +
187  "install.rdf is deprecated and will go away in a future version.");
188  feathersHub = feathersHub.feathers[0];
189  }
190 
191  if (feathersHub.skin) {
192  var skins = feathersHub.skin;
193  for (var j = 0; j < skins.length; j++) {
194  try {
195  this._registerSkin(addons[i], skins[j]);
196  }
197  catch (e) {
198  this._reportErrors("", [ "An error occurred while processing " +
199  "extension " + addons[i].Value + ". Exception: " + e ]);
200  }
201  }
202  }
203 
204  if (feathersHub.layout) {
205  var layouts = feathersHub.layout;
206  for (var j = 0; j < layouts.length; j++) {
207  try {
208  this._registerLayout(addons[i], layouts[j]);
209  }
210  catch (e) {
211  this._reportErrors("", [ "An error occurred while processing " +
212  "extension " + addons[i].Value + ". Exception: " + e ]);
213  }
214  }
215  }
216  }
217  },
218 
219 
223  _registerSkin: function _registerSkin(addon, skin) {
224  var description = new SkinDescription();
225 
226  // Array of error messages
227  var errorList = [];
228 
229  // NB: there is a "verify" function as well,
230  // but here we build and verify at the same time
231  for each (var prop in SkinDescription.prototype.requiredProperties) {
232  if(skin[prop][0]) {
233  description[prop] = skin[prop][0];
234  }
235  else {
236  errorList.push("Missing required <"+prop+"> element.");
237  }
238  }
239 
240  for each (var prop in SkinDescription.prototype.optionalProperties) {
241  if(skin[prop] && skin[prop][0]) {
242  description[prop] = skin[prop][0];
243  }
244  }
245 
246  // If errors were encountered, then do not submit
247  // to the Feathers Manager
248  if (errorList.length > 0) {
249  this._reportErrors(
250  "Ignoring skin addon in the install.rdf of extension " +
251  addon.Value + ". Message: ", errorList);
252  return;
253  }
254 
255  // Don't register skins that have no name, but throw up a friendly
256  // informative message so users don't see gross red error messages
257  // and misinterpret them.
258  if (!skin["name"]) {
259  var consoleSvc = Cc["@mozilla.org/consoleservice;1"]
260  .getService(Ci.nsIConsoleService);
261  consoleSvc.logStringMessage(
262  "Feathers Metadata Reader: Skipping registration of Feather with " +
263  "skin internal name: '" + skin["internalName"] + "' due to " +
264  "undefined or blank skin name.");
265  return;
266  }
267 
268  // Submit description
269  this._manager.registerSkin(description);
270  //debug("AddonMetadataReader: registered skin " + description.internalName
271  // + " from addon " + addon.Value + " \n");
272 
273  if (skin.compatibleLayout) {
274  var compatibleLayouts = skin.compatibleLayout;
275  var hasRegisteredDefault = false;
276  for (var i = 0; i < compatibleLayouts.length; i++) {
277  var compatibleLayout = compatibleLayouts[i];
278 
279  var layoutUrl;
280  // TODO: FIXME! this is inconsistent with other capitalizations!!
281  if(compatibleLayout.layoutURL && compatibleLayout.layoutURL[0].length != 0) {
282  layoutUrl = compatibleLayout.layoutURL[0];
283  }
284  else {
285  errorList.push("layoutUrl was missing or incorrect.");
286  continue;
287  }
288 
289  var showChrome = (PlatformUtils.platformString == "Darwin");
290  if (compatibleLayout.showChrome &&
291  compatibleLayout.showChrome[0] == "true") {
292  showChrome = true;
293  }
294  var onTop = false
295  if (compatibleLayout.onTop &&
296  compatibleLayout.onTop[0] == "true") {
297  onTop = true;
298  }
299 
300  this._manager.assertCompatibility(
301  layoutUrl,
302  description.internalName,
303  showChrome,
304  onTop
305  );
306 
307  // If this is the first element in the RDF - or the layout is set to
308  // be the default layout, let's assign these attributes here.
309  if ((i == 0) ||
310  (compatibleLayout.isDefault &&
311  compatibleLayout.isDefault[0] == "true"))
312  {
313  this._manager.setDefaultLayout(layoutUrl, description.internalName);
314 
315  if (hasRegisteredDefault) {
316  Components.utils.reportError(
317  "A default layout has already been assigned for " +
318  description.internalName
319  );
320  }
321 
322  hasRegisteredDefault = true;
323  }
324  }
325 
326  if (errorList.length > 0) {
327  this._reportErrors(
328  "Ignoring a <compatibleLayout> in the install.rdf of extension " +
329  addon.Value + ". Message: ", errorList);
330  return;
331  }
332  }
333  },
334 
338  _registerLayout: function _processLayout(addon, layout) {
339  var description = new LayoutDescription();
340 
341  // Array of error messages
342  var errorList = [];
343 
344  // NB: there is a "verify" function as well,
345  // but here we build and verify at the same time
346  for each (var prop in LayoutDescription.prototype.requiredProperties) {
347  if(layout[prop][0]) {
348  description[prop] = layout[prop][0];
349  }
350  else {
351  errorList.push("Missing required <"+prop+"> element.");
352  }
353  }
354 
355  for each (var prop in LayoutDescription.prototype.optionalProperties) {
356  if(layout[prop][0]) {
357  description[prop] = layout[prop][0];
358  }
359  }
360 
361  // If errors were encountered, then do not submit
362  // to the Feathers Manager
363  if (errorList.length > 0) {
364  this._reportErrors(
365  "Ignoring layout addon in the install.rdf of extension " +
366  addon.Value + ". Message: ", errorList);
367  return;
368  }
369 
370  // Resolve any localised layout names to their actual strings
371  if (description.name.substr(0,CHROME_PREFIX.length) == CHROME_PREFIX)
372  {
373  var name = SBString("feathers.name.unnamed");
374  var split = description.name.split("#", 2);
375  if (split.length == 2) {
376  var bundle = new SBStringBundle(split[0]);
377  name = bundle.get(split[1], name);
378  }
379  description.name = name;
380  }
381  // Submit description
382  this._manager.registerLayout(description);
383  //debug("AddonMetadataReader: registered layout " + description.name +
384  // " from addon " + addon.Value + "\n");
385 
386  // TODO: should we error out here if there are errors already?
387  if (layout.compatibleSkin) {
388  var compatibleSkins = layout.compatibleSkin;
389  for (var i = 0; i < compatibleSkins.length; i++) {
390  var compatibleSkin = compatibleSkins[i];
391 
392  var internalName;
393  if (compatibleSkin.internalName &&
394  compatibleSkin.internalName[0].length != 0)
395  {
396  internalName = compatibleSkin.internalName[0];
397  }
398  else {
399  errorList.push("internalName was missing or incorrect.");
400  continue;
401  }
402 
403  var showChrome = false;
404  if (compatibleSkin.showChrome &&
405  compatibleSkin.showChrome[0] == "true") {
406  showChrome = true;
407  }
408  var onTop = false;
409  if (compatibleSkin.onTop &&
410  compatibleSkin.onTop[0] == "true") {
411  onTop = true;
412  }
413 
414  this._manager.assertCompatibility(
415  description.url,
416  internalName,
417  showChrome,
418  onTop
419  );
420  }
421  }
422 
423  // Report errors
424  if (errorList.length > 0) {
425  this._reportErrors(
426  "Error finding compatibility information for layout " +
427  description.name + " in the install.rdf " +
428  "of extension " + addon.Value + ". Message: ", errorList);
429  }
430  },
431 
438  _reportErrors: function _reportErrors(contextMessage, errorList) {
439  for (var i = 0; i < errorList.length; i++) {
440  Components.utils.reportError("Feathers Metadata Reader: "
441  + contextMessage + errorList[i]);
442  }
443  }
444 }
445 
446 
447 
448 
449 
450 
451 
452 
453 
454 
455 
465 function FeathersManager() {
466 
467  this._observerSet = new ObserverSet();
468 
469  // We need to init at final UI startup after the Extension Manager has checked
470  // for feathers (required for NO_EM_RESTART support).
471  this._observerSet.add(this, "final-ui-startup", false, true);
472 
473  // We need to unhook things on shutdown
474  this._observerSet.add(this, "quit-application", false, true);
475 
476  this._skins = {};
477  this._layouts = {};
478  this._skinDefaults = {};
479  this._mappings = {};
480  this._listeners = [];
481 };
482 FeathersManager.prototype = {
484  classID: CID,
487  [{
488  category: "app-startup",
489  entry: "feathers-manager",
490  value: "service," + CONTRACTID
491  }],
492  constructor: FeathersManager,
493 
494  _observerSet: null,
495 
496  _layoutDataRemote: null,
497  _skinDataRemote: null,
498 
499  _previousLayoutDataRemote: null,
500  _previousSkinDataRemote: null,
501 
502  _showChromeDataRemote: null,
503 
504  _switching: false,
505 
506  // Ignore autoswitching when the feathers manager is run for the first time.
507  _ignoreAutoswitch: false,
508 
509  // feather, layout, and skin name defaults
510  _defaultLayoutURL: "",
511  _defaultSecondaryLayoutURL: "",
512  _defaultSkinName: "",
513 
514  // Hash of skin descriptions keyed by internalName (e.g. classic/1.0)
515  _skins: null,
516 
517  // Hash of layout descriptions keyed by URL
518  _layouts: null,
519 
520  // Hash of default layouts for skins.
521  _skinDefaults: null,
522 
523 
524 
525  // Hash of layout URL to hash of compatible skin internalNames, pointing to
526  // {showChrome,onTop} objects.
527  //
528  // eg
529  // {
530  // mainwin.xul: {
531  // blueskin: {showChrome:true, onTop:false},
532  // redskin: {showChrome:false, onTop:true},
533  // }
534  // }
535  //
536  // Compatibility is determined by whether or not a internalName
537  // key is *defined* in the hash, not the actual value it points to.
538  _mappings: null,
539 
540 
541  // Array of sbIFeathersChangeListeners
542  _listeners: null,
543 
544  _layoutCount: 0,
545  _skinCount: 0,
546 
547  // nsIURI, our agent sheet
548  _agentSheetURI: null,
549 
550 
560  _init: function init() {
561  // before reading the prefs, make sure they're stabilized; see bug
562  // 14504 for details.
563  Cc["@mozilla.org/browser/browserglue;1"]
564  .getService(Ci.nsIObserver)
565  .observe(null, "prefservice:after-app-defaults", null);
566 
567  var AppPrefs = Cc["@mozilla.org/fuel/application;1"]
568  .getService(Ci.fuelIApplication).prefs;
569 
570  // If the safe-mode dialog was requested to disable all addons, our
571  // basic layouts and default skin have been disabled too. We need to
572  // check if that's the case, and reenable them if needed
573  var defaultFeather = AppPrefs.getValue(PREF_DEFAULT_FEATHER_ID, "");
574  this._ensureAddOnEnabled(defaultFeather);
575 
576  // Read in defaults
577  this._defaultLayoutURL = AppPrefs.getValue(PREF_DEFAULT_MAIN_LAYOUT, "");
578  this._defaultSecondaryLayoutURL =
579  AppPrefs.getValue(PREF_DEFAULT_SECONDARY_LAYOUT, "");
580  this._defaultSkinName = AppPrefs.getValue(PREF_DEFAULT_SKIN_INTERNALNAME, "");
581 
582  if (!AppPrefs.has(PREF_FEATHERS_MANAGER_HAS_STARTED)) {
583  // Ignore the autoswitch detecting for the first run, this prevents
584  // jumping between two shipped feathers.
585  this._ignoreAutoswitch = true;
586 
587  // Now the feather manager has started at least once.
588  AppPrefs.setValue(PREF_FEATHERS_MANAGER_HAS_STARTED, true);
589  }
590 
591 
592  // Register our agent sheet for form styling
593  this._agentSheetURI = Cc["@mozilla.org/network/io-service;1"]
594  .getService(Ci.nsIIOService)
595  .newURI("chrome://songbird/skin/formsImport.css",
596  null, null);
597  var styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"]
598  .getService(Ci.nsIStyleSheetService);
599  styleSheetService.loadAndRegisterSheet(this._agentSheetURI,
600  styleSheetService.AGENT_SHEET);
601 
602  // Make dataremotes to persist feathers settings
603  var createDataRemote = new Components.Constructor(
604  "@songbirdnest.com/Songbird/DataRemote;1",
605  Ci.sbIDataRemote, "init");
606 
607  this._layoutDataRemote = createDataRemote("feathers.selectedLayout", null);
608  this._skinDataRemote = createDataRemote("selectedSkin", "general.skins.");
609 
610  this._previousLayoutDataRemote = createDataRemote("feathers.previousLayout", null);
611  this._previousSkinDataRemote = createDataRemote("feathers.previousSkin", null);
612 
613  // Check to make sure we have a skin set; if not, then set the current
614  // skin to be the default skin. If we don't have a default skin, then
615  // we'll really fubar'd. (bug 20528)
616  if (!this.currentSkinName) {
617  this._skinDataRemote.stringValue = this._defaultSkinName;
618  }
619  // TODO: Rename accessibility.enabled?
620  this._showChromeDataRemote = createDataRemote("accessibility.enabled", null);
621 
622  // Load the feathers metadata
623  var metadataReader = new AddonMetadataReader();
624  metadataReader.loadMetadata(this);
625 
626  // If no layout url has been specified, set to default
627  if (this._layoutDataRemote.stringValue == "") {
628  this._layoutDataRemote.stringValue = this._defaultLayoutURL;
629  }
630 
631  // Ensure chrome enabled is set appropriately
632  var showChrome = this.isChromeEnabled(this.currentLayoutURL,
633  this.currentSkinName);
634  this._setChromeEnabled(showChrome);
635  },
636 
640  _deinit: function deinit() {
641  this._observerSet.removeAll();
642  this._observerSet = null;
643  this._skins = null;
644  this._layouts = null;
645  this._mappings = null;
646  this._listeners = null;
647  this._layoutDataRemote = null;
648  this._skinDataRemote = null;
649  this._previousLayoutDataRemote = null;
650  this._previousSkinDataRemote = null;
651  this._showChromeDataRemote = null;
652  },
653 
657  get currentSkinName() {
658  return this._skinDataRemote.stringValue;
659  },
660 
661 
665  get currentLayoutURL() {
666  return this._layoutDataRemote.stringValue;
667  },
668 
669 
673  get previousSkinName() {
674  // Test to make sure the previous skin exists
675  var skin = this.getSkinDescription(this._previousSkinDataRemote.stringValue);
676 
677  // If the skin exists, then return the skin name
678  if (skin) {
679  return skin.internalName;
680  }
681 
682  // Otherwise, return the default skin
683  return this._defaultSkinName;
684  },
685 
686 
690  get previousLayoutURL() {
691  // Test to make sure the previous layout exists
692  var layout = this.getLayoutDescription(this._previousLayoutDataRemote.stringValue);
693 
694  // If the layout exists, then return the url/identifier
695  if (layout) {
696  return layout.url;
697  }
698 
699  // Otherwise, return the default
700 
701  // Use the main default unless it is currently
702  // active. This way if the user reverts for the
703  // first time they will end up in the miniplayer.
704  var layoutURL = this._defaultLayoutURL;
705  if (this.currentLayoutURL == layoutURL) {
706  layoutURL = this._defaultSecondaryLayoutURL;
707  }
708 
709  return layoutURL;
710  },
711 
712 
716  get skinCount() {
717  return this._skinCount;
718  },
719 
723  get layoutCount() {
724  return this._layoutCount;
725  },
726 
727 
731  getSkinDescriptions: function getSkinDescriptions() {
732  // Copy all the descriptions into an array, and then return an enumerator
733  return ArrayConverter.enumerator( [this._skins[key] for (key in this._skins)] );
734  },
735 
739  getLayoutDescriptions: function getLayoutDescriptions() {
740  // Copy all the descriptions into an array, and then return an enumerator
741  return ArrayConverter.enumerator( [this._layouts[key] for (key in this._layouts)] );
742  },
743 
744 
748  registerSkin: function registerSkin(skinDesc) {
749 
750  SkinDescription.verify(skinDesc);
751 
752  if (this._skins[skinDesc.internalName] == null) {
753  this._skinCount++;
754  }
755  this._skins[skinDesc.internalName] = skinDesc;
756 
757  // Notify observers
758  this._onUpdate();
759  },
760 
764  unregisterSkin: function unregisterSkin(skinDesc) {
765  if (this._skins[skinDesc.internalName]) {
766  delete this._skins[skinDesc.internalName];
767  this._skinCount--;
768 
769  // Notify observers
770  this._onUpdate();
771  }
772  },
773 
777  getSkinDescription: function getSkinDescription(internalName) {
778  return this._skins[internalName];
779  },
780 
781 
785  registerLayout: function registerLayout(layoutDesc) {
786  LayoutDescription.verify(layoutDesc);
787 
788  if (!(layoutDesc.url in this._layouts)) {
789  this._layoutCount++;
790  }
791  this._layouts[layoutDesc.url] = layoutDesc;
792 
793  // Notify observers
794  this._onUpdate();
795  },
796 
800  unregisterLayout: function unregisterLayout(layoutDesc) {
801  if (layoutDesc.url in this._layouts) {
802  delete this._layouts[layoutDesc.url];
803  this._layoutCount--;
804 
805  // Notify observers
806  this._onUpdate();
807  }
808  },
809 
813  getLayoutDescription: function getLayoutDescription(url) {
814  return (url in this._layouts ? this._layouts[url] : null);
815  },
816 
817 
821  assertCompatibility:
822  function assertCompatibility(layoutURL, internalName, aShowChrome, aOnTop) {
823  if (! (typeof(layoutURL) == "string" && typeof(internalName) == 'string')) {
824  throw Components.results.NS_ERROR_INVALID_ARG;
825  }
826  if (this._mappings[layoutURL] == null) {
827  this._mappings[layoutURL] = {};
828  }
829  this._mappings[layoutURL][internalName] = {showChrome: aShowChrome, onTop: aOnTop};
830 
831  // check if this layout/skin combination has already been seen,
832  // if it hasn't then we want to switch to it in openPlayerWindow,
833  // so remember it
834  var branch = this.getFeatherPrefBranch(layoutURL, internalName);
835  var seen = false;
836  try {
837  seen = branch.getBoolPref("seen");
838  } catch (e) { }
839  if (!seen) {
840  branch.setBoolPref("seen", true);
841  if (!this._autoswitch && !this._ignoreAutoswitch) {
842  this._autoswitch = {};
843  this._autoswitch.skin = internalName;
844  this._autoswitch.layoutURL = layoutURL;
845  }
846  }
847 
848  // Notify observers
849  this._onUpdate();
850  },
851 
855  unassertCompatibility: function unassertCompatibility(layoutURL, internalName) {
856  if (this._mappings[layoutURL]) {
857  delete this._mappings[layoutURL][internalName];
858 
859  // Notify observers
860  this._onUpdate();
861  }
862  },
863 
867  setDefaultLayout: function setDefaultLayout(aLayoutURL, aInternalName) {
868  if (!(typeof(aLayoutURL) == "string" &&
869  typeof(aInternalName) == "string"))
870  {
871  throw Components.results.NS_ERROR_INVALID_ARG;
872  }
873 
874  this._skinDefaults[aInternalName] = aLayoutURL;
875  this._onUpdate(); // notify observers
876  },
877 
881  getDefaultLayout: function getDefaultLayout(aInternalName) {
882  if (!typeof(aInternalName) == "string") {
883  throw Components.results.NS_ERROR_INVALID_ARG;
884  }
885 
886  var defaultLayoutURL = this._skinDefaults[aInternalName];
887 
888  // If a default URL isn't registered, just use the first compatible
889  // layout registered for the skin identifier.
890  if (!defaultLayoutURL) {
891  for (var curLayoutURL in this._mappings) {
892  if (aInternalName in this._mappings[curLayoutURL]) {
893  defaultLayoutURL = curLayoutURL;
894  break;
895  }
896  }
897  }
898 
899  // Something is terribly wrong - no layouts are registered for this skin
900  if (!defaultLayoutURL) {
901  throw Components.results.NS_ERROR_FAILURE;
902  }
903 
904  return defaultLayoutURL;
905  },
906 
910  isChromeEnabled: function isChromeEnabled(layoutURL, internalName) {
911  // TEMP fix for the Mac to enable the titlebar on the main window.
912  // See Bug 4363
913  var sysInfo = Cc["@mozilla.org/system-info;1"]
914  .getService(Ci.nsIPropertyBag2);
915  var platform = sysInfo.getProperty("name");
916 
917  if (this._mappings[layoutURL]) {
918  if (this._mappings[layoutURL][internalName]) {
919  return this._mappings[layoutURL][internalName].showChrome == true;
920  }
921  }
922 
923  return false;
924  },
925 
926 
927  getFeatherPrefBranch: function getFeatherPrefBranch (layoutURL, internalName) {
928  var prefs = Cc["@mozilla.org/preferences-service;1"]
929  .getService(Ci.nsIPrefService);
930 
931  // a really simple url escaping algorithm
932  // turn all non-alphanumeric characters into:
933  // "_" upper case hex charactre code "_"
934  function escape_url(url) {
935  return url.replace(/[^a-zA-Z0-9]/g,
936  function(c) {
937  return '_'+(c.charCodeAt(0).toString(16)).toUpperCase()+'_'; });
938  }
939 
940  var branchName = 'songbird.feather.' +
941  (internalName?internalName:'null') + '.' +
942  (layoutURL?escape_url(layoutURL):'null') + '.';
943 
944  return prefs.getBranch(branchName);
945  },
946 
947 
948  canOnTop: function canOnTop(layoutURL, internalName) {
949  if (this._mappings[layoutURL]) {
950  if (this._mappings[layoutURL][internalName]) {
951  return this._mappings[layoutURL][internalName].onTop == true;
952  }
953  }
954 
955  return false;
956  },
957 
958 
959  isOnTop: function isOnTop(layoutURL, internalName) {
960  if (!this.canOnTop(layoutURL, internalName)) {
961  return false;
962  }
963 
964  var prefBranch = this.getFeatherPrefBranch(layoutURL, null);
965  if (prefBranch.prefHasUserValue('on_top')) {
966  return prefBranch.getBoolPref('on_top');
967  }
968 
969  return false;
970  },
971 
972 
973  setOnTop: function setOnTop(layoutURL, internalName, onTop) {
974  if (!this.canOnTop(layoutURL, internalName)) {
975  return false;
976  }
977 
978  var prefBranch = this.getFeatherPrefBranch(layoutURL, null);
979  prefBranch.setBoolPref('on_top', onTop);
980 
981  return;
982  },
983 
984 
985  /* FIXME: add the ability to observe onTop state */
986 
987 
991  getSkinsForLayout: function getSkinsForLayout(layoutURL) {
992  var skins = [];
993 
994  // Find skin descriptions that are compatible with the given layout.
995  if (this._mappings[layoutURL]) {
996  for (internalName in this._mappings[layoutURL]) {
997  var desc = this.getSkinDescription(internalName);
998  if (desc) {
999  skins.push(desc);
1000  }
1001  }
1002  }
1003  return ArrayConverter.enumerator( skins );
1004  },
1005 
1006 
1010  getLayoutsForSkin: function getLayoutsForSkin(internalName) {
1011  return ArrayConverter.enumerator( this._getLayoutsArrayForSkin(internalName) );
1012  },
1013 
1014 
1018  switchFeathers: function switchFeathers(layoutURL, internalName) {
1019  // don't allow this call if we're already switching
1020  if (this._switching) {
1021  return;
1022  }
1023 
1024  layoutDescription = this.getLayoutDescription(layoutURL);
1025  skinDescription = this.getSkinDescription(internalName);
1026 
1027  // Make sure we know about the requested skin and layout
1028  if (layoutDescription == null || skinDescription == null) {
1029  throw new Components.Exception("Unknown layout/skin passed to switchFeathers");
1030  }
1031 
1032  // Check compatibility.
1033  // True/false refer to the showChrome value, so check for undefined
1034  // to determine compatibility.
1035  if (this._mappings[layoutURL][internalName] === undefined) {
1036  throw new Components.Exception("Skin [" + internalName + "] and Layout [" + layoutURL +
1037  " are not compatible");
1038  }
1039 
1040  // Notify that a select is about to occur
1041  this._onSelect(layoutDescription, skinDescription);
1042 
1043  // Remember the current feathers so that we can revert later if needed
1044  this._previousLayoutDataRemote.stringValue = this.currentLayoutURL;
1045  this._previousSkinDataRemote.stringValue = this.currentSkinName;
1046 
1047  // Make sure the application doesn't exit because we closed the last window
1048  var appStartup = Cc["@mozilla.org/toolkit/app-startup;1"]
1049  .getService(Ci.nsIAppStartup);
1050  appStartup.enterLastWindowClosingSurvivalArea();
1051 
1052  try {
1053  // close the player window *before* changing the skin
1054  // otherwise Gecko tries to load an image that will go away right after and crashes
1055  // (songbird bug 3965)
1056  this._closePlayerWindow(internalName == this.currentSkinName);
1057 
1058  var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
1059  var callback = new FeathersManager_switchFeathers_callback(this, layoutURL, internalName);
1060  this._switching = true;
1061  timer.initWithCallback(callback, 0, Ci.nsITimer.TYPE_ONE_SHOT);
1062  } catch (e) {
1063  // If we failed make sure to allow the application to quit again
1064  appStartup.exitLastWindowClosingSurvivalArea();
1065  throw e;
1066  }
1067  },
1068 
1072  switchToNextLayout: function switchToNextLayout() {
1073  var curSkinName = this.currentSkinName;
1074  var curLayoutURL = this.currentLayoutURL;
1075 
1076  // Find the next layout (if one exists):
1077  var nextLayout;
1078  var layouts = this._getLayoutsArrayForSkin(curSkinName);
1079  for (var i = 0; i < layouts.length; i++) {
1080  if (layouts[i].url == curLayoutURL) {
1081  if (i >= layouts.length - 1) {
1082  nextLayout = layouts[0];
1083  }
1084  else {
1085  nextLayout = layouts[i+1];
1086  }
1087  }
1088  }
1089 
1090  if (nextLayout != null && nextLayout.url != curLayoutURL) {
1091  this.switchFeathers(nextLayout.url, curSkinName);
1092  }
1093  },
1094 
1095 
1100  openPlayerWindow: function openPlayerWindow() {
1101  // First, check if we should auto switch to a new skin/layout
1102  // (but only if we're not already in the middle of a switch)
1103  if (this._autoswitch && !this._switching) {
1104  this._layoutDataRemote.stringValue = this._autoswitch.layoutURL;
1105  this._skinDataRemote.stringValue = this._autoswitch.skin;
1106  this._autoswitch = null;
1107  }
1108 
1109  // Check to make sure the current feathers are valid
1110  var layoutDescription = this.getLayoutDescription(this.currentLayoutURL);
1111  var skinDescription = this.getSkinDescription(this.currentSkinName);
1112  if (layoutDescription == null || skinDescription == null) {
1113  // The current feathers are invalid. Switch to the defaults.
1114  this.switchFeathers(this._defaultLayoutURL, this._defaultSkinName);
1115  return;
1116  }
1117 
1118  var currentLayoutURL = this.currentLayoutURL;
1119  var currentSkinName = this.currentSkinName;
1120 
1121  // check if we're in safe mode
1122  var app = Cc["@mozilla.org/xre/app-info;1"]
1123  .getService(Ci.nsIXULRuntime);
1124  if (app.inSafeMode) {
1125  // in safe mode, force using default layout/skin
1126  // (but do not persist this choice)
1127  currentLayoutURL = this._defaultLayoutURL;
1128  currentSkinName = this._defaultSkinName;
1129  }
1130 
1131  // Check to see if we are in test mode, if so, we don't actually
1132  // want to open the window as it will break the testing we're
1133  // attempting to do.
1134  if(SBDataGetBoolValue(DATAREMOTE_TESTMODE)) {
1135  // Indicate to the console and jsconsole that we are test mode.
1136  dump("FeathersManager.openPlayerWindow: In Test Mode, no window will be open!\n");
1137  Cu.reportError("FeathersManager.openPlayerWindow: In Test Mode, no window will be open\n");
1138  return;
1139  }
1140 
1141  // Determine window features. If chrome is enabled, make resizable.
1142  // Otherwise remove the titlebar.
1143  var chromeFeatures = "chrome,modal=no,resizable=yes,toolbar=yes,popup=no";
1144 
1145  // on windows and mac, centerscreen gets overriden by persisted position.
1146  // not so for linux.
1147  var runtimeInfo = Components.classes["@mozilla.org/xre/runtime;1"]
1148  .getService(Components.interfaces.nsIXULRuntime);
1149  switch (runtimeInfo.OS) {
1150  case "WINNT":
1151  case "Darwin":
1152  chromeFeatures += ",centerscreen";
1153  }
1154 
1155  var showChrome = this.isChromeEnabled(currentLayoutURL, currentSkinName);
1156  if (showChrome) {
1157  chromeFeatures += ",titlebar=yes";
1158  } else {
1159  chromeFeatures += ",titlebar=no";
1160  }
1161 
1162  // Set the global chrome (window border and title) flag
1163  this._setChromeEnabled(showChrome);
1164 
1165  // Open the new player window
1166  var windowWatcher = Cc["@mozilla.org/embedcomp/window-watcher;1"]
1167  .getService(Ci.nsIWindowWatcher);
1168 
1169  var newMainWin = windowWatcher.openWindow(null,
1170  currentLayoutURL,
1171  "",
1172  chromeFeatures,
1173  null);
1174  newMainWin.focus();
1175  },
1176 
1177 
1181  addListener: function addListener(listener) {
1182  if (! (listener instanceof Ci.sbIFeathersManagerListener))
1183  {
1184  throw Components.results.NS_ERROR_INVALID_ARG;
1185  }
1186  this._listeners.push(listener);
1187  },
1188 
1192  removeListener: function removeListener(listener) {
1193  var index = this._listeners.indexOf(listener);
1194  if (index > -1) {
1195  this._listeners.splice(index,1);
1196  }
1197  },
1198 
1199 
1203  _getLayoutsArrayForSkin: function _getLayoutsArrayForSkin(internalName) {
1204  var layouts = [];
1205 
1206  // Find skin descriptions that are compatible with the given layout.
1207  for (var layout in this._mappings) {
1208  if (internalName in this._mappings[layout]) {
1209  var desc = this.getLayoutDescription(layout);
1210  if (desc) {
1211  layouts.push(desc);
1212  }
1213  }
1214  }
1215 
1216  return layouts;
1217  },
1218 
1219 
1227  _closePlayerWindow: function _closePlayerWindow(aLayoutSwitchOnly) {
1228  // Check to see if we are in test mode, if so, we don't actually
1229  // want to open the window as it will break the testing we're
1230  // attempting to do.
1231  if(SBDataGetBoolValue(DATAREMOTE_TESTMODE)) {
1232  // Indicate to the console and jsconsole that we are test mode.
1233  dump("FeathersManager.openPlayerWindow: In Test Mode\n");
1234  Cu.reportError("FeathersManager.openPlayerWindow: In Test Mode\n");
1235  return;
1236  }
1237 
1238  var windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"]
1239  .getService(Ci.nsIWindowMediator);
1240 
1241  // Close all open windows other than the core, dominspector, and venkman.
1242  // This is needed in order to reset window chrome settings.
1243  var playerWindows = windowMediator.getEnumerator(null);
1244  while (playerWindows.hasMoreElements()) {
1245  var window = playerWindows.getNext();
1246  if (!window) {
1247  continue;
1248  }
1249 
1250  // Don't close DOMi or other debug windows... that's just annoying
1251  try {
1252  let docElement = window.document.documentElement;
1253  // Go by the window ID instead of URL for these, in case we end up with
1254  // things that try to override them (e.g. Error2)
1255  switch (docElement.getAttribute("id")) {
1256  case "JSConsoleWindow":
1257  case "winInspectorMain":
1258  case "venkman-window":
1259  // don't close these
1260  continue;
1261  }
1262  if (aLayoutSwitchOnly) {
1263  // this is a layout switch, the skin will be the same;
1264  // check for opt-in attribute and skip those
1265  if (docElement.hasAttribute("sb-no-close-on-layout-switch")) {
1266  // don't close this either
1267  continue;
1268  }
1269  }
1270  } catch (e) {
1271  /* ignore any errors - assume they're closable */
1272  }
1273 
1274  window.close();
1275  }
1276  },
1277 
1278 
1283  _setChromeEnabled: function _setChromeEnabled(enabled) {
1284 
1285  // Set the global chrome (window border and title) flag
1286  this._showChromeDataRemote.boolValue = enabled;
1287 
1288  var prefs = Cc["@mozilla.org/preferences-service;1"]
1289  .getService(Ci.nsIPrefBranch);
1290 
1291  // Set the flags used to open the core window on startup.
1292  // Do a replacement in order to preserve whatever other features
1293  // were specified.
1294  try {
1295  var titlebarRegEx = /(titlebar=)(no|yes)/;
1296  var replacement = (enabled) ? "$1yes" : "$1no";
1297  var defaultChromeFeatures = prefs.getCharPref("toolkit.defaultChromeFeatures");
1298  prefs.setCharPref("toolkit.defaultChromeFeatures",
1299  defaultChromeFeatures.replace(titlebarRegEx, replacement));
1300  } catch (e) {
1301  Cu.reportError("FeathersManager._setChromeEnabled: Error setting " +
1302  "defaultChromeFeatures pref!\n" + e);
1303  }
1304  },
1305 
1306 
1310  _onUpdate: function onUpdate() {
1311  this._listeners.forEach( function (listener) {
1312  listener.onFeathersUpdate();
1313  });
1314  },
1315 
1316 
1320  _onSelect: function onSelect(layoutDesc, skinDesc) {
1321  // Verify args
1322  layoutDesc = layoutDesc.QueryInterface(Ci.sbILayoutDescription);
1323  skinDesc = skinDesc.QueryInterface(Ci.sbISkinDescription);
1324 
1325  // Broadcast notification
1326  this._listeners.forEach( function (listener) {
1327  listener.onFeathersSelectRequest(layoutDesc, skinDesc);
1328  });
1329  },
1330 
1331  _onSelectComplete: function onSelectComplete() {
1332  var layoutDescription = this.getLayoutDescription(this.currentLayoutURL);
1333  var skinDescription = this.getSkinDescription(this.currentSkinName);
1334 
1335  // Broadcast notification
1336  this._listeners.forEach( function (listener) {
1337  listener.onFeathersSelectComplete(layoutDescription, skinDescription);
1338  });
1339  },
1340 
1344  observe: function(subject, topic, data) {
1345  var os = Cc["@mozilla.org/observer-service;1"]
1346  .getService(Ci.nsIObserverService);
1347  switch (topic) {
1348  case "quit-application":
1349  this._deinit();
1350  break;
1351 
1352  case "final-ui-startup":
1353  this._init();
1354  break;
1355  }
1356  },
1357 
1364  _ensureAddOnEnabled: function(id) {
1365  const nsIUpdateItem = Ci.nsIUpdateItem;
1366  var em = Cc["@mozilla.org/extensions/manager;1"]
1367  .getService(Ci.nsIExtensionManager);
1368  var ds = em.datasource;
1369  var rdf = Cc["@mozilla.org/rdf/rdf-service;1"]
1370  .getService(Ci.nsIRDFService);
1371 
1372  var resource = rdf.GetResource("urn:mozilla:item:" + id);
1373 
1374  var property = rdf.GetResource("http://www.mozilla.org/2004/em-rdf#userDisabled");
1375  var target = ds.GetTarget(resource, property, true);
1376 
1377  function getData(literalOrResource) {
1378  if (literalOrResource instanceof Ci.nsIRDFLiteral ||
1379  literalOrResource instanceof Ci.nsIRDFResource ||
1380  literalOrResource instanceof Ci.nsIRDFInt)
1381  return literalOrResource.Value;
1382  return undefined;
1383  }
1384 
1385  var userDisabled = getData(target);
1386 
1387  if (userDisabled == "true") {
1388  em.enableItem(id);
1389  }
1390  },
1391 
1396  XPCOMUtils.generateQI([IID, Components.interfaces.nsIObserver]),
1397 }; // FeathersManager.prototype
1398 
1404  aLayoutURL,
1405  aInternalName) {
1406  this.feathersManager = aFeathersManager;
1407  this.layoutURL = aLayoutURL;
1408  this.internalName = aInternalName;
1409 }
1410 
1415  notify: function FeathersManager_switchFeathers_callback_notify() {
1416  try {
1417  // Set new values
1418  this.feathersManager._layoutDataRemote.stringValue = this.layoutURL;
1419  this.feathersManager._skinDataRemote.stringValue = this.internalName;
1420 
1421  // Flush all chrome caches
1422  Cc["@mozilla.org/observer-service;1"]
1423  .getService(Ci.nsIObserverService)
1424  .notifyObservers(null, "chrome-flush-caches", null);
1425 
1426  // Reload our agent sheet
1427  var styleSheetService = Cc["@mozilla.org/content/style-sheet-service;1"]
1428  .getService(Ci.nsIStyleSheetService);
1429  styleSheetService.unregisterSheet(this.feathersManager._agentSheetURI,
1430  styleSheetService.AGENT_SHEET);
1431  styleSheetService.loadAndRegisterSheet(this.feathersManager._agentSheetURI,
1432  styleSheetService.AGENT_SHEET);
1433 
1434  this.feathersManager.openPlayerWindow();
1435  this.feathersManager._switching = false;
1436  this.feathersManager._onSelectComplete();
1437  this.feathersManager = null;
1438  } finally {
1439  // After the callback the application should always be able to quit again
1440  var appStartup = Cc["@mozilla.org/toolkit/app-startup;1"]
1441  .getService(Ci.nsIAppStartup);
1442  appStartup.exitLastWindowClosingSurvivalArea();
1443  }
1444  }
1445 }; // FeathersManager_switchFeathers_callback.prototype
1446 
1447 
1448 
1449 
1450 
1457 function NSGetModule(comMgr, fileSpec) {
1458  return XPCOMUtils.generateModule([FeathersManager]);
1459 } // NSGetModule
1460 
1461 
1462 
classDescription entry
Definition: FeedWriter.js:1427
const Cc
SkinDescription prototype
menuItem id
Definition: FeedWriter.js:971
const WINDOWTYPE_SONGBIRD_CORE
function FeathersManager_switchFeathers_callback(aFeathersManager, aLayoutURL, aInternalName)
const IID
sbDeviceFirmwareAutoCheckForUpdate prototype contractID
const RDFURI_ADDON_ROOT
sbOSDControlService prototype QueryInterface
sbDeviceFirmwareAutoCheckForUpdate prototype classDescription
const PREF_DEFAULT_FEATHER_ID
function NSGetModule(comMgr, fileSpec)
const PREF_DEFAULT_SKIN_INTERNALNAME
function LayoutDescription()
var titlebar
Definition: mainwin.js:127
const PREF_DEFAULT_SECONDARY_LAYOUT
function SBString(aKey, aDefault, aStringBundle)
Definition: StringUtils.jsm:93
let window
const PREF_FEATHERS_MANAGER_HAS_STARTED
function RDFHelper(aRdf, aDatasource, aResource, aNamespaces)
Definition: RDFHelper.jsm:61
TimerLoop prototype notify
_window init
Definition: FeedWriter.js:1144
const Cu
function SBDataGetBoolValue(aKey)
Get the value of the data in boolean format.
var bundle
var skins
const WINDOWTYPE_SONGBIRD_PLAYER
DataRemote prototype constructor
const Cr
function SBStringBundle(aBundle)
var createDataRemote
grep callback
return null
Definition: FeedWriter.js:1143
const Ci
var layouts
const PREF_DEFAULT_MAIN_LAYOUT
const CHROME_PREFIX
var os
function url(spec)
var prefs
Definition: FeedWriter.js:1169
countRef value
Definition: FeedWriter.js:1423
var feathersManager
const PREFIX_NS_SONGBIRD
observe topic
Definition: FeedWriter.js:1326
sbDeviceFirmwareAutoCheckForUpdate prototype classID
sbWindowsAutoPlayServiceCfg _xpcom_categories
const CONTRACTID
_dialogDatepicker onSelect
const CLASSNAME
observe data
Definition: FeedWriter.js:1329
window addListener("unload", function(){window.removeListener("unload", arguments.callee);document.purge();if(Browser.Engine.trident){CollectGarbage();}})
_getSelectedPageStyle s i
const CID
function SkinDescription()
sbDeviceFirmwareAutoCheckForUpdate prototype observe