sbMediaPageManager.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 
25 const Cc = Components.classes;
26 const Ci = Components.interfaces;
27 const Cr = Components.results;
28 const Ce = Components.Exception;
29 const Cu = Components.utils;
30 
31 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
32 Cu.import("resource://app/jsmodules/ArrayConverter.jsm");
33 Cu.import("resource://app/jsmodules/RDFHelper.jsm");
34 Cu.import("resource://app/jsmodules/sbProperties.jsm");
35 
36 function MediaPageManager() {
37 }
38 
39 MediaPageManager.prototype = {
40  classDescription: "Songbird MediaPage Manager",
41  classID: Components.ID("{e63463d0-357c-4035-af33-db670ee1b7f2}"),
42  contractID: "@songbirdnest.com/Songbird/MediaPageManager;1",
43  QueryInterface: XPCOMUtils.generateQI([Ci.sbIMediaPageManager]),
44 
45  _pageInfoArray: [],
46 
47  // PageInfo objects for the fallback mediapages. Set by _registerDefaults.
48  _defaultPlaylistPage: null,
49  _defaultFilteredPlaylistPage: null,
50 
51 
52  registerPage: function(aName, aURL, aIcon, aMatchInterface) {
53  // Make sure we don't already have a page with
54  // the given url
55  var pageInfo;
56 
57  for each (pageInfo in this._pageInfoArray) {
58  if (pageInfo.contentUrl == aURL) {
59  throw new Error("Page URL already registered: " + aURL);
60  }
61  }
62 
63  // Make a PageInfo object
64  pageInfo = {
65  get contentTitle() {
66  return aName;
67  },
68  get contentUrl() {
69  return aURL;
70  },
71  get contentIcon() {
72  return aIcon;
73  },
74  get matchInterface() {
75  return aMatchInterface;
76  },
77  QueryInterface: function(iid) {
78  if (!iid.equals(Ci.sbIMediaPageInfo) &&
79  !iid.equals(Ci.nsISupports))
80  throw Cr.NS_ERROR_NO_INTERFACE;
81  return this;
82  }
83  };
84 
85  // Store the page into our page array
86  this._pageInfoArray.push(pageInfo);
87 
88  // And return it to the caller so he may use that to unregister
89  return pageInfo;
90  },
91 
92  unregisterPage: function(aPageInfo) {
93 
94  // If unregistering the default pages, must remove shortcut pointers
95  if (this._defaultFilteredPlaylistPage &&
96  this._defaultFilteredPlaylistPage.contentUrl == aPageInfo.contentUrl) {
97  this._defaultFilteredPlaylistPage = null;
98  } else if (this._defaultPlaylistPage &&
99  this._defaultPlaylistPage.contentUrl == aPageInfo.contentUrl) {
100  this._defaultPlaylistPage = null;
101  }
102 
103  // Search the array for the matching page
104  for (var i in this._pageInfoArray) {
105  if (aPageInfo.contentUrl == this._pageInfoArray[i].contentUrl) {
106  // found, remove it and stop
107  this._pageInfoArray.splice(i, 1);
108  return;
109  }
110  }
111  // Page not found, throw!
112  throw new Error("Page " + aPageInfo.contentTitle + " not found in unregisterPage");
113  },
114 
115  getAvailablePages: function(aList, aConstraint) {
116  this._ensureMediaPageRegistration();
117  // If no list is provided, return the entire set
118  if (!aList) {
119  return ArrayConverter.enumerator(this._pageInfoArray);
120  }
121  // Otherwise, make a list of what matches the list
122  var tempArray = [];
123  for (var i in this._pageInfoArray) {
124  var pageInfo = this._pageInfoArray[i];
125  if (pageInfo.matchInterface.match(aList, aConstraint)) {
126  tempArray.push(pageInfo);
127  }
128  }
129 
130  // ... and return that.
131  return ArrayConverter.enumerator(tempArray);
132  },
133 
134  getPage: function(aList, aConstraint, aType) {
135  this._ensureMediaPageRegistration();
136 
137  // use the outermost list
138  aList = this._getOutermostList(aList);
139 
140  // Read the saved state
141  var remote = Cc["@songbirdnest.com/Songbird/DataRemote;1"]
142  .createInstance(Ci.sbIDataRemote);
143  var baseKey = "mediapages." + aList.guid;
144  var key = baseKey;
145  if (aType)
146  key = baseKey + "." + aType;
147  remote.init(key, null);
148  var savedPageURL = remote.stringValue;
149  if (savedPageURL && savedPageURL != "") {
150  // Check that the saved url is still registered
151  // and still supports this list
152  let pageInfo = this._checkPageForList(aList, aConstraint, savedPageURL);
153  if (pageInfo) return pageInfo;
154  }
155  // fall back to prefs with no type
156  else if (aType) {
157  let remote = Cc["@songbirdnest.com/Songbird/DataRemote;1"]
158  .createInstance(Ci.sbIDataRemote);
159  remote.init(baseKey, null);
160  let savedOldPageURL = remote.stringValue;
161  if (savedOldPageURL && savedOldPageURL != "") {
162  // Check that the saved url is still registered
163  // and still supports this list
164  let oldPageInfo = this._checkPageForList(aList,
165  aConstraint,
166  savedOldPageURL);
167  if (oldPageInfo) return oldPageInfo;
168  }
169  }
170 
171  // Read the list's default
172  var defaultPageURL = aList.getProperty(SBProperties.defaultMediaPageURL);
173  if (defaultPageURL && defaultPageURL != "") {
174  // Check that the saved url is still registered
175  // and still supports this list
176  var pageInfo = this._checkPageForList(aList, aConstraint, defaultPageURL);
177  if (pageInfo) return pageInfo;
178  }
179 
180  // No saved state and no default, this is either the first time this list
181  // is shown, or its previous saved/default page isn't valid anymore, so
182  // pick a new one
183 
184  // Hardcoded first run logic:
185  // Everybody gets the listview (playlistPage)
186  if (this._defaultPlaylistPage) {
187  return this._defaultPlaylistPage;
188  } else {
189  // No hardcoded defaults. Look for anything
190  // that matches.
191  for (var i in this._pageInfoArray) {
192  var pageInfo = this._pageInfoArray[i];
193  if (pageInfo.matchInterface.match(aList, aConstraint)) {
194  return pageInfo;
195  }
196  }
197  }
198 
199  // Oh crap.
200  throw new Error("MediaPageManager unable to determine a page for " + aList.guid);
201 
202  // keep js happy ?
203  return null;
204  },
205 
206  setPage: function(aList, aPageInfo, aType) {
207  // use the outermost list
208  aList = this._getOutermostList(aList);
209 
210  // Save the state
211  var remote = Cc["@songbirdnest.com/Songbird/DataRemote;1"]
212  .createInstance(Ci.sbIDataRemote);
213  var key = "mediapages." + aList.guid;
214  if (aType)
215  key = key + "." + aType;
216  remote.init(key, null);
217  remote.stringValue = aPageInfo.contentUrl;
218  },
219 
220  // internal, get the outermost list for a given list. for instance, when
221  // given a smart medialist's storage list, this returns the original
222  // smart medialist.
223  _getOutermostList: function(aList) {
224  var outerGuid = aList.getProperty(SBProperties.outerGUID);
225  if (outerGuid)
226  aList = aList.library.getMediaItem(outerGuid);
227  return aList;
228  },
229 
230  // internal. checks that a url is registered in the list of pages, and that
231  // its matching test succeeds for a given list
232  _checkPageForList: function(aList, aConstraint, aUrl) {
233  for (var i in this._pageInfoArray) {
234  var pageInfo = this._pageInfoArray[i];
235  if (pageInfo.contentUrl != aUrl) continue;
236  if (!pageInfo.matchInterface.match(aList, aConstraint)) continue;
237  return pageInfo;
238  }
239  return null;
240  },
241 
242  _ensureMediaPageRegistration: function() {
243  if(this._registrationComplete) { return };
244 
245  this._registerDefaults();
246  MediaPageMetadataReader.loadMetadata(this);
247 
248  this._registrationComplete = true;
249  },
250 
251  _registerDefaults: function() {
252  var playlistString = "mediapages.playlistpage";
253  var filteredPlaylistString = "mediapages.filteredplaylistpage";
254  try {
255  var stringBundleService = Cc["@mozilla.org/intl/stringbundle;1"]
256  .getService(Ci.nsIStringBundleService);
257  var stringBundle = stringBundleService.createBundle(
258  "chrome://songbird/locale/songbird.properties" );
259  playlistString = stringBundle.GetStringFromName(playlistString);
260  filteredPlaylistString = stringBundle.GetStringFromName(
261  filteredPlaylistString);
262  stringBundleService = null;
263  stringBundle = null;
264  } catch (e) {
265  Component.utils.reportError("MediaPageManager: Couldn't localize default media page name.\n")
266  }
267 
268  // the default page matches everything
269  var matchAll = {match: function() true};
270 
271  // Register the playlist with filters
272  this._defaultFilteredPlaylistPage =
273  this.registerPage( filteredPlaylistString,
274  "chrome://songbird/content/mediapages/filtersPage.xul",
275  null,
276  matchAll);
277 
278  // And the playlist without filters
279  this._defaultPlaylistPage =
280  this.registerPage( playlistString,
281  "chrome://songbird/content/mediapages/playlistPage.xul",
282  null,
283  matchAll);
284  },
285 
286 }; // MediaPageManager.prototype
287 
288 
289 
294 var MediaPageMetadataReader = {
295  loadMetadata: function(manager) {
296  this._manager = manager;
297 
298  var addons = RDFHelper.help(
299  "rdf:addon-metadata",
300  "urn:songbird:addon:root",
301  RDFHelper.DEFAULT_RDF_NAMESPACES
302  );
303 
304  for (var i = 0; i < addons.length; i++) {
305  // skip addons with no panes.
306  if (!addons[i].mediaPage)
307  continue;
308  try {
309  var pages = addons[i].mediaPage;
310  for (var j = 0; j < pages.length; j++) {
311  this._registerMediaPage(addons[i], pages[j])
312  }
313  } catch (e) {
314  this._reportErrors("", [ "An error occurred while processing " +
315  "extension " + addons[i].Value + ". Exception: " + e ]);
316  }
317  }
318  },
319 
323  _registerMediaPage: function _registerMediaPage(addon, page) {
324  // create and validate our page info
325  var errorList = [];
326  var warningList = [];
327 
328  var info = {};
329  for (property in page) {
330  if (page[property])
331  info[property] = page[property][0];
332  }
333 
334  this._validateProperties(info, errorList, warningList);
335 
336  // create a match function
337  var matchFunction;
338  if (page.match) {
339  var matchList = this._createMatchList(page, warningList);
340  matchFunction = this._createMatchFunction(matchList);
341  }
342  else {
343  matchFunction = this._createMatchAllFunction();
344  }
345 
346  // If errors were encountered, then do not submit
347  if (warningList.length > 0){
348  this._reportErrors(
349  "Warning: " + addon.Value + " install.rdf loading media page: " , warningList);
350  }
351  if (errorList.length > 0) {
352  this._reportErrors(
353  "ERROR: " + addon.Value + " install.rdf IGNORED media page: ", errorList);
354  return;
355  }
356 
357  // Submit description
358  this._manager.registerPage( info.contentTitle,
359  info.contentUrl,
360  info.contentIcon,
361  {match: matchFunction}
362  );
363 
364  //dump("MediaPageMetadataReader: registered pane " + info.contentTitle
365  // + " at " + info.contentUrl + " from addon " + addon.Value + " \n");
366  },
367 
368  _validateProperties: function(info, errorList, warningList) {
369  var requiredProperties = ["contentTitle", "contentUrl"];
370  var optionalProperties = ["contentIcon", "match"];
371 
372  // check for required properties
373  for (var p in requiredProperties) {
374  if (!info[requiredProperties[p]]) {
375  errorList.push("Missing required property " + requiredProperties[p] + ".\n")
376  }
377  }
378 
379  // check for unused RDF nodes and warn about them.
380  var template = {};
381  for(var i in requiredProperties) {
382  template[requiredProperties[i]] = "required";
383  }
384  for(var i in optionalProperties) {
385  template[optionalProperties[i]] = "optional";
386  }
387 
388  for (var p in info) {
389  if (!template[p]) {
390  warningList.push("Unrecognized property " + p + ".\n")
391  }
392  }
393  },
394 
395  _createMatchList: function(page, warningList) {
396  // create a set of property-comparison objects
397  // one for each <match>. a mediapage will work for medialists which match
398  // all the properties in a hash.
399  var matchList = [];
400 
401  for (var i = 0; i < page.match.length; i++) {
402  var fields = page.match[i].split(/\s+/);
403  var properties = {}
404  for(var f in fields) {
405  var key; var value;
406  [key, value] = fields[f].split(":");
407  if(!value) { value = key; key = "type" };
408 
409  // TODO: quoted string values ala name:"My Funky List"
410  // TODO: check if the key is actually a legal key-type and warn if not
411 
412  if(properties[key]) {
413  warningList.push("Attempting to match two values for "
414  +key+": "+properties[key]+" and "+value+".");
415  }
416 
417  properties[key] = value;
418  }
419 
420  matchList.push(properties);
421  }
422 
423  return matchList;
424  },
425 
426  _createMatchFunction: function(matchList) {
427  // create a match function that uses the match options
428  var matchFunction = function(mediaList, aConstraint) {
429  // just in case someone's passing in bad values
430  if(!mediaList) {
431  return false;
432  }
433 
434  // check each <match/>'s values
435  // if any one set works, this is a good media page for the list
436  for (var m in matchList) {
437  let match = matchList[m];
438 
439  // first, see if we just want to opt out of this match
440  // our definition of an opt-out-able list is:
441  // one that is so unspecific as to only target by "type"
442  // or to target everything. (see below)
443  // (this is a bit natty)
444  var numProperties = 0;
445  for (var i in match) {
446  numProperties++
447  }
448  // if we *only* target the "type" of the list
449  // and the list wants to opt out
450  if (numProperties == 1 && match["type"]) {
451  if (mediaList.getProperty(SBProperties.onlyCustomMediaPages) == "1") {
452  return false;
453  }
454  }
455 
456  var thisListMatches = true;
457  for (var i in match) {
458  switch(i) {
459  case "contentType":
460  // this is set on the constraint instead
461  if (aConstraint) {
462  for (let group in ArrayConverter.JSEnum(aConstraint.groups)) {
463  if (!(group instanceof Ci.sbILibraryConstraintGroup)) {
464  continue;
465  }
466  if (!group.hasProperty(SBProperties.contentType)) {
467  continue;
468  }
469  let contentTypes =
470  ArrayConverter.JSArray(group.getValues(SBProperties.contentType));
471  if (contentTypes.indexOf(match[i]) == -1) {
472  thisListMatches = false;
473  break;
474  }
475  }
476  }
477  break;
478  default:
479  if (i in mediaList) {
480  thisListMatches &= (match[i] == mediaList[i]);
481  }
482  else {
483  // Use getProperty notation if the desired field is not
484  // specified as a true JS property on the object.
485  // TODO: This should be improved.
486  thisListMatches &= (match[i] == mediaList.getProperty(SBProperties[i]));
487  }
488  }
489  if (!thisListMatches) {
490  break;
491  }
492  }
493  if (thisListMatches) {
494  return true;
495  }
496  }
497 
498  // arriving here means none of the <match>
499  // elements fit the medialist passed in
500  return false;
501  }
502 
503  return matchFunction;
504  },
505 
506  _createMatchAllFunction: function() {
507  var matchFunction = function(mediaList) {
508  // opt-out lists will also exclude completely generic pages
509  // this detail must be clearly communicated to the MP dev'rs!
510  return(mediaList && mediaList.getProperty(SBProperties.onlyCustomMediaPages) != "1");
511  };
512  return matchFunction;
513  },
514 
521  _reportErrors: function _reportErrors(contextMessage, errorList) {
522  var consoleService = Cc["@mozilla.org/consoleservice;1"]
523  .getService(Ci.nsIConsoleService);
524  for (var i = 0; i < errorList.length; i++) {
525  Cu.reportError("MediaPage Addon Metadata: " + contextMessage + errorList[i]);
526  }
527  }
528 }
529 
530 function NSGetModule(compMgr, fileSpec) {
531  return XPCOMUtils.generateModule([MediaPageManager]);
532 }
533 
function MediaPageManager()
const Ci
sbDeviceFirmwareAutoCheckForUpdate prototype contractID
sbOSDControlService prototype QueryInterface
const Cr
sbDeviceFirmwareAutoCheckForUpdate prototype classDescription
function RDFHelper(aRdf, aDatasource, aResource, aNamespaces)
Definition: RDFHelper.jsm:61
const Cu
ExtensionSchemeMatcher prototype match
return null
Definition: FeedWriter.js:1143
_updateCookies aName
countRef value
Definition: FeedWriter.js:1423
const Cc
sbDeviceFirmwareAutoCheckForUpdate prototype classID
_getSelectedPageStyle s i
var group
const Ce