DropHelper.jsm
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 /*
26 
27  DropHelper Module
28 
29  This module contains three different helpers:
30 
31  - ExternalDropHandler
32 
33  Used to handle external drops (ie, standard file drag and drop from the
34  operating system).
35 
36  - InternalDropHandler
37 
38  Used to handle internal drops (ie, mediaitems, medialists)
39 
40  - DNDUtils
41 
42  Contains methods that are used by both of the helpers, and that are useful for
43  drag and drop operations in general.
44 
45 */
46 
47 EXPORTED_SYMBOLS = [ "DNDUtils",
48  "ExternalDropHandler",
49  "InternalDropHandler" ];
50 
51 const Cc = Components.classes;
52 const Ci = Components.interfaces;
53 const Cr = Components.results;
54 const Ce = Components.Exception;
55 const Cu = Components.utils;
56 
58  "chrome://songbird/skin/base-elements/icon-generic-addon.png";
59 
60 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
61 
62 Cu.import("resource://app/jsmodules/ArrayConverter.jsm");
63 Cu.import("resource://app/jsmodules/sbProperties.jsm");
64 Cu.import("resource://app/jsmodules/SBJobUtils.jsm");
65 Cu.import("resource://app/jsmodules/sbLibraryUtils.jsm");
66 Cu.import("resource://app/jsmodules/SBUtils.jsm");
67 Cu.import("resource://app/jsmodules/StringUtils.jsm");
68 
70  "@songbirdnest.com/Songbird/Library/medialistduplicatefilter;1";
71 /*
72 
73  DNDUtils
74 
75  This helper contains a number of methods that may be used when implementing
76  a drag and drop handler.
77 
78 */
79 
80 var DNDUtils = {
81 
82  // returns true if the drag session contains supported flavors
83  isSupported: function(aDragSession, aFlavourArray) {
84  for (var i=0;i<aFlavourArray.length;i++) {
85  if (aDragSession.isDataFlavorSupported(aFlavourArray[i])) {
86  return true;
87  }
88  }
89  return false;
90  },
91 
92  // adds the flavors in the array to the given flavourset
93  addFlavours: function(aFlavourSet, aFlavourArray) {
94  for (var i=0; i<aFlavourArray.length; i++) {
95  aFlavourSet.appendFlavour(aFlavourArray[i]);
96  }
97  },
98 
99  // fills an array with the data for all items of a given flavor
100  getTransferDataForFlavour: function(aFlavour, aSession, aArray) {
101  if (!aSession) {
102  var dragService = Cc["@mozilla.org/widget/dragservice;1"]
103  .getService(Ci.nsIDragService);
104  aSession = dragService.getCurrentSession();
105  }
106 
107  var nitems = aSession.numDropItems;
108  var r = null;
109 
110  if (aSession.isDataFlavorSupported(aFlavour)) {
111  var transfer = Cc["@mozilla.org/widget/transferable;1"]
112  .createInstance(Ci.nsITransferable);
113  transfer.addDataFlavor(aFlavour);
114 
115  for (var i=0;i<nitems;i++) {
116  aSession.getData(transfer, i);
117  var data = {};
118  var length = {};
119  transfer.getTransferData(aFlavour, data, length);
120  if (!r) r = data.value;
121  if (aArray) aArray.push([data.value, length.value, aFlavour]);
122  }
123  }
124 
125  return r;
126  },
127 
128  // similar to getTransferDataForFlavour but adds extraction of the internal
129  // drag data from the dndSourceTracker, and queries an interface from the
130  // result
131  getInternalTransferDataForFlavour: function(aSession, aFlavour, aInterface) {
132  var data = this.getTransferDataForFlavour(aFlavour, aSession);
133  if (data)
134  return this.getInternalTransferData(data, aInterface);
135  return null;
136  },
137 
138  // similar to getTransferData but adds extraction of the internal drag data
139  // from the dndSourceTracker, and queries an interface from the result
140  getInternalTransferData: function(aData, aInterface) {
141  // get the object from the dnd source tracker
142  var dnd = Components.classes["@songbirdnest.com/Songbird/DndSourceTracker;1"]
143  .getService(Ci.sbIDndSourceTracker);
144  var source = dnd.getSourceSupports(aData);
145  // and request the specified interface
146  return source.QueryInterface(aInterface);
147  },
148 
149  // returns an array with the data for any flavour listed in the given array
150  getTransferData: function(aSession, aFlavourArray) {
151  // I know this is confusing but because numDropItems is a Number it causes
152  // the Array constructor to pre-allocate N slots which should speed up
153  // pushing elements into it.
154  var data = new Array(aSession.numDropItems);
155  for (var i=0;i<aFlavourArray.length;i++) {
156  if (this.getTransferDataForFlavour(aFlavourArray[i], aSession, data))
157  break;
158  }
159  return data;
160  },
161 
162  // reports a custom temporary message to the status bar
163  customReport: function(aMessage) {
164  var SB_NewDataRemote =
165  Components.Constructor("@songbirdnest.com/Songbird/DataRemote;1",
166  "sbIDataRemote",
167  "init");
168  var statusOverrideText =
169  SB_NewDataRemote( "faceplate.status.override.text", null );
170  var statusOverrideType =
171  SB_NewDataRemote( "faceplate.status.override.type", null );
172 
173  statusOverrideText.stringValue = "";
174  statusOverrideText.stringValue = aMessage;
175  statusOverrideType.stringValue = "report";
176  },
177 
178  // temporarily writes "X tracks added to <name>, Y tracks already present"
179  // in the status bar. if 0 is specified for aDups, the second part of the
180  // message is skipped.
181  reportAddedTracks: function(aAdded, aDups, aUnsupported, aDestName, aIsDevice) {
182  var msg = "";
183 
184  /* We only report D&D status for non-device-related transfers
185  * Please see bug 23763 if status bar reporting for D&D onto a device is
186  * being implemented for notes from sneumann.
187  */
188  if (!aIsDevice) {
189  if (aDups && aUnsupported) {
190  msg = SBFormattedString("library.tracksadded.with.dups.and.unsupported",
191  [aAdded, aDestName, aDups, aUnsupported]);
192  }
193  else if (aDups) {
194  msg = SBFormattedString("library.tracksadded.with.dups",
195  [aAdded, aDestName, aDups]);
196  }
197  else if (aUnsupported) {
198  msg = SBFormattedString("library.tracksadded.with.unsupported",
199  [aAdded, aDestName, aUnsupported]);
200  }
201  else {
202  msg = SBFormattedString("library.tracksadded",
203  [aAdded, aDestName]);
204  }
205  }
206  this.customReport(msg);
207  },
208 
209  // reports stats on the statusbar using standard rules for what to show and
210  // in which circumstances
211  standardReport: function(aTargetList,
212  aImportedInLibrary,
213  aDuplicates,
214  aUnsupported,
215  aInsertedInMediaList,
216  aOtherDropsHandled,
217  aDevice) {
218  // do not report anything if all we did was drop an XPI
219  if ((aImportedInLibrary == 0) &&
220  (aInsertedInMediaList == 0) &&
221  (aDuplicates == 0) &&
222  (aUnsupported == 0) &&
223  (aOtherDropsHandled != 0))
224  return;
225 
226  // report different things depending on whether we dropped
227  // on a library, or just a list
228  if (aTargetList != aTargetList.library) {
229  DNDUtils.reportAddedTracks(aInsertedInMediaList,
230  0,
231  aUnsupported,
232  aTargetList.name,
233  aDevice);
234  } else {
235  DNDUtils.reportAddedTracks(aImportedInLibrary,
236  aDuplicates,
237  aUnsupported,
238  aTargetList.name,
239  aDevice);
240  }
241  }
242 }
243 
244 /* MediaListViewSelectionTransferContext
245  *
246  * Create a drag and drop context containing the selected items in the view
247  * which is passed in.
248  * Implements: sbIMediaItemsTransferContext
249  */
250 DNDUtils.MediaListViewSelectionTransferContext = function (mediaListView) {
251  this.items = null; // filled in during reset()
252  this.indexedItems = null;
253  this.source = mediaListView.mediaList;
254  this.count = mediaListView.selection.count;
255  this._mediaListView = mediaListView;
256  this.reset();
257 };
258 DNDUtils.MediaListViewSelectionTransferContext.prototype = {
259  reset: function() {
260  // Create an enumerator that unwraps the sbIIndexedMediaItem enumerator
261  // which selection provides.
262  var enumerator = Cc["@songbirdnest.com/Songbird/Library/EnumeratorWrapper;1"]
263  .createInstance(Ci.sbIMediaListEnumeratorWrapper);
264  enumerator.initialize(this._mediaListView
265  .selection
266  .selectedIndexedMediaItems);
267  this.items = enumerator;
268 
269  // and here's the wrapped form for those cases where you want it
270  this.indexedItems = this._mediaListView.selection.selectedIndexedMediaItems;
271  },
272  QueryInterface : function(iid) {
273  if (iid.equals(Components.interfaces.sbIMediaItemsTransferContext) ||
274  iid.equals(Components.interfaces.nsISupports))
275  return this;
276  throw Components.results.NS_NOINTERFACE;
277  }
278  };
279 
280 /* EntireMediaListViewTransferContext
281  *
282  * Create a drag and drop context containing all the items in the view
283  * which is passed in.
284  * Implements: sbIMediaItemsTransferContext
285  */
286 DNDUtils.EntireMediaListViewTransferContext = function(view) {
287  this.items = null;
288  this.indexedItems = null;
289  this.source = view.mediaList;
290  this.count = view.length;
291  this._mediaListView = view;
292  this.reset();
293  }
294 DNDUtils.EntireMediaListViewTransferContext.prototype = {
295  reset: function() {
296  // Create an ugly pseudoenumerator
297  var that = this;
298  this.items = {
299  i: 0,
300  hasMoreElements : function() {
301  return this.i < that._mediaListView.length;
302  },
303  getNext : function() {
304  var item = that._mediaListView.getItemByIndex(this.i++);
305  item.setProperty(SBProperties.downloadStatusTarget,
306  item.library.guid + "," + item.guid);
307  return item;
308  },
309  QueryInterface : function(iid) {
310  if (iid.equals(Components.interfaces.nsISimpleEnumerator) ||
311  iid.equals(Components.interfaces.nsISupports))
312  return this;
313  throw Components.results.NS_NOINTERFACE;
314  }
315  };
316  },
317  QueryInterface : function(iid) {
318  if (iid.equals(Components.interfaces.sbIMediaItemsTransferContext) ||
319  iid.equals(Components.interfaces.nsISupports))
320  return this;
321  throw Components.results.NS_NOINTERFACE;
322  }
323 };
324 
325 /* MediaListTransferContext
326  *
327  * A transfer context suitable for moving a single media list around the system.
328  * As of this writing, the only place to create a drag/drop session of a single
329  * media list is the service pane, though it is also possible that extension
330  * developers will create them in the playlist widget.
331  */
332 DNDUtils.MediaListTransferContext = function (item, mediaList) {
333  this.item = item;
334  this.list = item;
335  this.source = mediaList;
336  this.count = 1;
337  }
338 DNDUtils.MediaListTransferContext.prototype = {
339  QueryInterface : function(iid) {
340  if (iid.equals(Components.interfaces.sbIMediaListTransferContext) ||
341  iid.equals(Components.interfaces.nsISupports))
342  return this;
343  throw Components.results.NS_NOINTERFACE;
344  }
345  }
346 
347 /*
348 
349  InternalDropHandler
350 
351 
352 This helper is used to let you handle internal drops (mediaitems and medialists)
353 and inject the items into a medialist, potentially at a specific position.
354 
355 There are two ways of triggering a drop handling, the question of which one you
356 should be using depends on how it is you would like to handle the drop:
357 
358 To handle a drop in a generic manner, and have all dropped items automatically
359 directed to the default library, all you need to do is add the following code in
360 your onDrop/ondragdrop handler:
361 
362  InternalDropHandler.drop(window, dragSession, dropHandlerListener);
363 
364 The last parameter is optional, it allows you to receive notifications. Here is
365 a minimal implementation:
366 
367  var dropHandlerListener = {
368  // called when the drop handling has completed
369  onDropComplete: function(aTargetList,
370  aImportedInLibrary,
371  aDuplicates,
372  aUnsupported,
373  aInsertedInMediaList,
374  aOtherDropsHandled) {
375  // returning true causes the standard drop report to be printed
376  // on the status bar, it is equivalent to calling standardReport
377  // using the parameters received on this callback
378  return true;
379  },
380  // called when the first item is handled (eg, to implement playback)
381  onFirstMediaItem: function(aTargetList, aFirstMediaItem) { }
382  // called when a medialist has been copied from a different source library
383  onCopyMediaList: function(aSourceList, aNewList) { }
384  };
385 
386 To handle a drop with a specific mediaList target and drop insertion point, use
387 the following code:
388 
389  InternalDropHandler.dropOnList(window,
390  dragSession,
391  targetMediaList,
392  targetMediaListPosition,
393  dropHandlerListener);
394 
395 In order to target the drop at the end of the targeted mediaList, you
396 should give a value of -1 for targetMediaListPosition.
397 
398 The other public methods in this helper can be used to simplify the rest of your
399 drag and drop handler as well. For instance, an nsDragAndDrop observer's
400 getSupportedFlavours() method may be implemented simply as:
401 
402  var flavours = new FlavourSet();
403  InternalDropHandler.addFlavours(flavours);
404  return flavours;
405 
406 Also, getTransferData, getInternalTransferDataForFlavour, and
407 getTransferDataForFlavour may be used to inspect the content of the dragSession
408 before deciding what to do with it.
409 
410 */
411 
412 const TYPE_X_SB_TRANSFER_MEDIA_ITEM = "application/x-sb-transfer-media-item";
413 const TYPE_X_SB_TRANSFER_MEDIA_LIST = "application/x-sb-transfer-media-list";
414 const TYPE_X_SB_TRANSFER_MEDIA_ITEMS = "application/x-sb-transfer-media-items";
415 
416 var InternalDropHandler = {
417 
418  supportedFlavours: [ TYPE_X_SB_TRANSFER_MEDIA_ITEM,
421 
422  // returns true if the drag session contains supported internal flavors
423  isSupported: function(aDragSession) {
424  return DNDUtils.isSupported(aDragSession, this.supportedFlavours);
425  },
426 
427  // performs a default drop of the drag session. media items go to the
428  // main library.
429  drop: function(aWindow, aDragSession, aListener) {
430  var mainLibrary = Cc["@songbirdnest.com/Songbird/library/Manager;1"]
431  .getService(Ci.sbILibraryManager)
432  .mainLibrary;
433  this.dropOnList(aWindow, aDragSession, mainLibrary, -1, aListener);
434  },
435 
436  // perform a drop onto a medialist. media items are inserted at the specified
437  // position in the list if that list is orderable. otherwise, or if the
438  // position is invalid, the items are added to the target list.
439  dropOnList: function(aWindow,
440  aDragSession,
441  aTargetList,
442  aDropPosition,
443  aListener) {
444  if (!aTargetList) {
445  throw new Error("No target medialist specified for dropOnList");
446  }
447  this._dropItems(aDragSession,
448  aTargetList,
449  aDropPosition,
450  aListener);
451  },
452 
453  // call this to automatically add the supported internal flavors
454  // to a flavourSet object
455  addFlavours: function(aFlavourSet) {
456  DNDUtils.addFlavours(aFlavourSet, this.supportedFlavours);
457  },
458 
459  // returns an array with the data for any supported internal flavor
460  getTransferData: function(aSession) {
461  return DNDUtils.getTransferData(aSession, this.supportedFlavours);
462  },
463 
464  // simply forward the call. here in this object for completeness
465  // see DNDUtils.getTransferDataForFlavour for more info
466  getTransferDataForFlavour: function(aFlavour, aSession, aArray) {
467  return DNDUtils.getTransferDataForFlavour(aFlavour, aSession, aArray);
468  },
469 
470  // --------------------------------------------------------------------------
471  // methods below this point are pretend-private
472  // --------------------------------------------------------------------------
473 
485  _dropItems: function(aDragSession, aTargetList, aDropPosition, aListener) {
486  // are we dropping a media list ?
487  if (aDragSession.isDataFlavorSupported(TYPE_X_SB_TRANSFER_MEDIA_LIST)) {
488  this._dropItemsList(aDragSession, aTargetList, aDropPosition, aListener);
489  } else if (aDragSession.isDataFlavorSupported(TYPE_X_SB_TRANSFER_MEDIA_ITEMS)) {
490  this._dropItemsItems(aDragSession, aTargetList, aDropPosition, aListener);
491  }
492  },
493 
497  _doesDeviceSupportPlaylist: function(aDevice) {
498  // Check the device capabilities to see if it supports playlists.
499  // Device implementations may respond to CONTENT_PLAYLIST for either
500  // FUNCTION_DEVICE or FUNCTION_AUDIO_PLAYBACK.
501  var capabilities = aDevice.capabilities;
502  var sbIDC = Ci.sbIDeviceCapabilities;
503  try {
504  if (capabilities.supportsContent(sbIDC.FUNCTION_DEVICE,
505  sbIDC.CONTENT_PLAYLIST) ||
506  capabilities.supportsContent(sbIDC.FUNCTION_AUDIO_PLAYBACK,
507  sbIDC.CONTENT_PLAYLIST)) {
508  return true;
509  }
510  } catch (e) {}
511 
512  // couldn't find PLAYLIST support in either the DEVICE
513  // or AUDIO_PLAYBACK category
514  return false;
515  },
516 
517  _getTransferForDeviceChanges: function(aDevice, aSourceItems, aSourceList,
518  aDestLibrary) {
519  var differ = Cc["@songbirdnest.com/Songbird/Device/DeviceLibrarySyncDiff;1"]
520  .createInstance(Ci.sbIDeviceLibrarySyncDiff);
521 
522  var sourceLibrary;
523  if (aSourceItems)
524  sourceLibrary = aSourceItems.queryElementAt(0, Ci.sbIMediaItem).library;
525  else
526  sourceLibrary = aSourceList.library;
527 
528  var changeset = {};
529  var destItems = {};
530  differ.generateDropLists(sourceLibrary,
531  aDestLibrary,
532  aSourceList,
533  aSourceItems,
534  destItems,
535  changeset);
536 
537  return {changeset: changeset.value, items: destItems.value};
538  },
539 
540  _notifyListeners: function(aListener, aTargetList, aNewList, aSourceItems,
541  aSourceList, aIsDevice) {
542  var sourceLength = 0;
543 
544  if (aSourceList)
545  sourceLength = aSourceList.length;
546  else if (aSourceItems)
547  sourceLength = aSourceItems.length;
548 
549  // Let our listeners know.
550  if (aListener) {
551  if (aNewList)
552  aListener.onCopyMediaList(aTargetList, aNewList);
553 
554  if (sourceLength) {
555  if (aSourceList)
556  aListener.onFirstMediaItem(aSourceList.getItemByIndex(0));
557  else if (aSourceItems)
558  aListener.onFirstMediaItem(
559  aSourceItems.queryElementAt(0, Ci.sbIMediaItem));
560  }
561  }
562 
563  // These values are bogus, at least if the target is a library (and hence
564  // already-existing items won't be copied). Better than nothing?
565  this._dropComplete(aListener,
566  aTargetList,
567  sourceLength,
568  0,
569  0,
570  sourceLength,
571  0,
572  aIsDevice);
573  },
574 
575  // aItem.library doesn't return the device library. Find the matching one
576  // from the device.
577  _getDeviceLibraryForItem: function(aDevice, aItem) {
578  var lib = aItem.library;
579 
580  var libs = aDevice.content.libraries;
581  for (var i = 0; i < libs.length; i++) {
582  var deviceLib = libs.queryElementAt(i, Ci.sbIDeviceLibrary);
583  if (lib.equals(deviceLib))
584  return deviceLib;
585  }
586 
587  return null;
588  },
589 
590  // Transfer a dropped list where the source is a device, and the destination
591  // is not.
592  _dropListFromDevice: function(aDevice, aSourceList, aTargetList,
593  aDropPosition, aListener)
594  {
595  // Find out what changes need making.
596  var changes = this._getTransferForDeviceChanges(aDevice,
597  null, aSourceList, aTargetList.library);
598  var changeset = changes.changeset;
599 
600  var targetLibrary = aTargetList.library;
601 
602  aDevice.importFromDevice(targetLibrary, changeset);
603 
604  // Get the list that was created, if any.
605  var newlist = null;
606  var changes = changeset.changes;
607  for (var i = 0; i < changes.length; i++) {
608  var change = changes.queryElementAt(i, Ci.sbILibraryChange);
609  if (change.itemIsList) {
610  if (change.operation == Ci.sbIChangeOperation.ADD) {
611  var originGUID =
612  change.sourceItem.getProperty(SBProperties.originItemGuid);
613  newlist = targetLibrary.getItemByGuid(originGUID);
614  break;
615  }
616  }
617  }
618 
619  this._notifyListeners(aListener, aTargetList, newlist,
620  null, aSourceList, true);
621  },
622 
623  _dropItemsFromDevice: function(aDevice, aSourceItems, aTargetList,
624  aDropPosition, aListener)
625  {
626  // Find out what changes need making.
627  var changes = this._getTransferForDeviceChanges(aDevice,
628  aSourceItems, null, aTargetList.library);
629  var changeset = changes.changeset;
630 
631  var targetLibrary = aTargetList.library;
632 
633  aDevice.importFromDevice(targetLibrary, changeset);
634 
635  if (aTargetList.library != aTargetList) {
636  // This was a drop on an existing playlist. Now the items need
637  // adding to the playlist - changes.items is an nsIArray containing these
638  if (aTargetList instanceof Ci.sbIOrderableMediaList &&
639  aDropPosition != -1) {
640  aTargetList.insertSomeBefore(aDropPosition,
641  ArrayConverter.enumerator(changes.items));
642  }
643  else {
644  aTargetList.addSome(ArrayConverter.enumerator(changes.items));
645  }
646  }
647 
648  this._notifyListeners(aListener, aTargetList, null,
649  aSourceItems, null, true);
650  },
651 
652 
653  // Transfer a dropped list to a device.
654  //
655  // aTargetList may be a device library or a device playlist.
656  _dropListOnDevice: function(aDevice, aSourceList, aTargetList,
657  aDropPosition, aListener) {
658  // Find out what changes need making.
659  var changes = this._getTransferForDeviceChanges(aDevice,
660  null, aSourceList, aTargetList.library);
661  var changeset = changes.changeset;
662 
663  var deviceLibrary = this._getDeviceLibraryForItem(aDevice, aTargetList);
664 
665  // Apply the changes to get the actual media items onto the device, even
666  // if the device doesn't support playlists. This will add the items to the
667  // device library and schedule all the necessary file copies/etc. This
668  // also creates the device-side list if appropriate.
669  aDevice.exportToDevice(deviceLibrary, changeset);
670 
671  // Get the list that was created, if any.
672  var newlist = null;
673  var changes = changeset.changes;
674  for (var i = 0; i < changes.length; i++) {
675  var change = changes.queryElementAt(i, Ci.sbILibraryChange);
676  if (change.itemIsList) {
677  if (change.operation == Ci.sbIChangeOperation.ADD) {
678  var foundLists =
679  this.deviceLibrary.getItemsByProperty(SBProperties.originItemGuid,
680  change.sourceItem.guid);
681  if (foundLists.length > 0) {
682  newlist = foundLists[0];
683  }
684  break;
685  }
686  }
687  }
688 
689  this._notifyListeners(aListener, aTargetList, newlist,
690  null, aSourceList, true);
691  },
692 
693  // Transfer dropped items to a device.
694  //
695  // aTargetList may be a device library or a device playlist.
696  _dropItemsOnDevice: function(aDevice, aSourceItems, aTargetList,
697  aDropPosition, aListener) {
698  // Find out what changes need making.
699  var changes = this._getTransferForDeviceChanges(aDevice,
700  aSourceItems, null, aTargetList.library);
701  var changeset = changes.changeset;
702 
703  var deviceLibrary = this._getDeviceLibraryForItem(aDevice, aTargetList);
704 
705  // Apply the changes to get the media items onto the device
706  // if the device doesn't support playlists. This will add the items to the
707  // device library and schedule all the necessary file copies/etc.
708  aDevice.exportToDevice(deviceLibrary, changeset);
709 
710  if (aTargetList.library != aTargetList) {
711  // This was a drop on an existing device playlist. Now the items need
712  // adding to the playlist - changes.items is an nsIArray containing these
713  if (aTargetList instanceof Ci.sbIOrderableMediaList &&
714  aDropPosition != -1) {
715  aTargetList.insertSomeBefore(aDropPosition,
716  ArrayConverter.enumerator(changes.items));
717  }
718  else {
719  aTargetList.addSome(ArrayConverter.enumerator(changes.items));
720  }
721  }
722 
723  this._notifyListeners(aListener, aTargetList, null,
724  aSourceItems, null, true);
725  },
726 
727  // Transfer dropped items, where neither source or destination is on a device
728  _dropItemsSimple: function(aItems, aTargetList, aDropPosition, aListener) {
729  if (aTargetList instanceof Ci.sbIOrderableMediaList &&
730  aDropPosition != -1) {
731  aTargetList.insertSomeBefore(aDropPosition,
732  ArrayConverter.enumerator(aItems));
733  }
734  else {
735  aTargetList.addSome(ArrayConverter.enumerator(aItems));
736  }
737 
738  this._notifyListeners(aListener, aTargetList, null, aItems, null, false);
739  },
740 
741  // Drop from a list to another list, where neither list is on a device.
742  _dropListSimple: function(aSourceList, aTargetList,
743  aDropPosition, aListener) {
744  var newlist = null;
745 
746  var targetIsLibrary = (aTargetList instanceof Ci.sbILibrary);
747  if (targetIsLibrary) {
748  newlist = aTargetList.copyMediaList('simple', aSourceList, false);
749  }
750  else {
751  if (aTargetList instanceof Ci.sbIOrderableMediaList &&
752  aDropPosition != -1)
753  {
754  aTargetList.insertAllBefore(aDropPosition, aSourceList);
755  }
756  else {
757  aTargetList.addAll(aSourceList);
758  }
759  }
760 
761  this._notifyListeners(aListener, aTargetList, newlist, null, aSourceList,
762  false);
763  },
764 
770  _dropItemsList: function(aDragSession, aTargetList,
771  aDropPosition, aListener) {
772  var context = DNDUtils.
773  getInternalTransferDataForFlavour(aDragSession,
774  TYPE_X_SB_TRANSFER_MEDIA_LIST,
775  Ci.sbIMediaListTransferContext);
776  var sourceList = context.list;
777  if (sourceList == aTargetList) {
778  // uh oh - you can't drop a list onto itself
779  this._dropComplete(aListener, aTargetList, 0, context.count, 0, 0, 0,
780  false);
781  return;
782  }
783 
784  // Check if our destination is on a device; some behaviour differs if it is.
785  var deviceManager = Cc["@songbirdnest.com/Songbird/DeviceManager;2"]
786  .getService(Ci.sbIDeviceManager2);
787  var destDevice = deviceManager.getDeviceForItem(aTargetList);
788  var sourceDevice = deviceManager.getDeviceForItem(sourceList);
789 
790  if (destDevice) {
791  // We use heavily customised behaviour if the target is a device.
792  if (aTargetList.library == aTargetList) {
793  // Drop onto a library
794  this._dropListOnDevice(destDevice, sourceList, aTargetList,
795  aDropPosition, aListener);
796  }
797  else {
798  // Drop onto a playlist in a library. This should actually be
799  // treated as a drop of the ITEMS in the source list.
800  items = this._itemsFromList(sourceList);
801  this._dropItemsOnDevice(destDevice, items, aTargetList,
802  aDropPosition, aListener);
803  }
804  }
805  else if (sourceDevice) {
806  // We use special D&D behaviour for dragging a list _from_ a device too.
807  if (aTargetList.library == aTargetList) {
808  // Drop onto a library
809  this._dropListFromDevice(sourceDevice, sourceList, aTargetList,
810  aDropPosition, aListener);
811  }
812  else {
813  // Drop onto a playlist in a library; treat as a drop of the items.
814  items = this._itemsFromList(sourceList);
815  this._dropItemsFromDevice(destDevice, items, aTargetList,
816  aDropPosition, aListener);
817  }
818  }
819  else {
820  // No devices are involved, we just need the simple behaviour.
821  this._dropListSimple(sourceList, aTargetList,
822  aDropPosition, aListener);
823  }
824  },
825 
826  // Get an nsIArray of sbIMediaItems from a media list
827  _itemsFromList: function(list)
828  {
829  var items = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
830  var listener = {
831  onEnumerationBegin : function(aMediaList) {
832  return Ci.sbIMediaListEnumerationListener.CONTINUE;
833  },
834  onEnumeratedItem : function(aMediaList, aMediaItem) {
835  items.appendElement(aMediaItem, false);
836  return Ci.sbIMediaListEnumerationListener.CONTINUE;
837  },
838  onEnumerationEnd : function(aMediaList, aStatusCode) {
839  }
840  };
841  list.enumerateAllItems(listener, Ci.sbIMediaList.ENUMERATIONTYPE_SNAPSHOT);
842 
843  return items;
844  },
845 
846  // Get an nsIArray of sbIMediaItems from an nsISimpleEnumerator of same.
847  _itemsFromEnumerator: function(itemEnum) {
848  var items = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
849 
850  while (itemEnum.hasMoreElements())
851  items.appendElement(itemEnum.getNext(), false);
852 
853  return items;
854  },
855 
861  _dropItemsItems: function(aDragSession, aTargetList,
862  aDropPosition, aListener) {
863  var context = DNDUtils.
864  getInternalTransferDataForFlavour(aDragSession,
866  Ci.sbIMediaItemsTransferContext);
867 
868  var itemEnumerator = context.items;
869 
870  var items = this._itemsFromEnumerator(itemEnumerator);
871 
872  // are we dropping on a library? Assume all source items are from the same
873  // library.
874  if (aTargetList instanceof Ci.sbILibrary) {
875  if (items.length > 0 &&
876  items.queryElementAt(0, Ci.sbIMediaItem).library == aTargetList)
877  {
878  // can't add items to a library to which they already belong
879  this._dropComplete(aListener, aTargetList, 0, context.count, 0, 0, 0,
880  false);
881  return;
882  }
883  }
884 
885  var deviceManager = Cc["@songbirdnest.com/Songbird/DeviceManager;2"]
886  .getService(Ci.sbIDeviceManager2);
887  var destDevice = deviceManager.getDeviceForItem(aTargetList);
888  var sourceDevice = null;
889  if (items.length > 0)
890  sourceDevice = deviceManager.getDeviceForItem(
891  items.queryElementAt(0, Ci.sbIMediaItem));
892 
893  if (destDevice) {
894  // We use heavily customised behaviour if the target is a device.
895  this._dropItemsOnDevice(destDevice, items, aTargetList,
896  aDropPosition, aListener);
897  }
898  else if (sourceDevice) {
899  this._dropItemsFromDevice(sourceDevice, items, aTargetList,
900  aDropPosition, aListener);
901  }
902  else {
903  // If the source is a device, or no devices are involved, we just need
904  // the simple behaviour.
905  this._dropItemsSimple(items, aTargetList,
906  aDropPosition, aListener);
907  }
908  },
909 
910  // called when the whole drop handling operation has completed, used
911  // to notify the original caller and free up any resources we can
912  _dropComplete: function(listener,
913  targetList,
914  totalImported,
915  totalDups,
916  totalUnsupported,
917  totalInserted,
918  otherDrops,
919  isDevice) {
920  // notify the listener that we're done
921  if (listener) {
922  if (listener.onDropComplete(targetList,
923  totalImported,
924  totalDups,
925  totalUnsupported,
926  totalInserted,
927  otherDrops)) {
928  DNDUtils.standardReport(targetList,
929  totalImported,
930  totalDups,
931  totalUnsupported,
932  totalInserted,
933  otherDrops,
934  isDevice);
935  }
936  } else {
937  DNDUtils.standardReport(targetList,
938  totalImported,
939  totalDups,
940  totalUnsupported,
941  totalInserted,
942  otherDrops,
943  isDevice);
944  }
945  },
946 }
947 
948 /*
949 
950 
951  ExternalDropHandler JSM Module
952 
953 
954 
955 This helper is used to let you handle external file drops and automatically
956 handle scanning directories as needed, injecting items at a specific spot in
957 a media list, schedule a metadata scanner job for the newly imported items,
958 and so on.
959 
960 There are two ways of triggering a drop handling, the question of which one you
961 should be using depends on how it is you would like to handle the drop:
962 
963 To handle a drop in a generic manner, and have all dropped items automatically
964 directed to the default library, all you need to do is add the following code in
965 your onDrop/ondragdrop handler:
966 
967  ExternalDropHandler.drop(window, dragSession, dropHandlerListener);
968 
969 The last parameter is optional, it allows you to receive notifications. Here is
970 a minimal implementation:
971 
972  var dropHandlerListener = {
973  // called when the drop handling has completed
974  onDropComplete: function(aTargetList,
975  aImportedInLibrary,
976  aDuplicates,
977  aInsertedInMediaList,
978  aOtherDropsHandled) {
979  // returning true causes the standard drop report to be printed
980  // on the status bar, it is equivalent to calling standardReport
981  // using the parameters received on this callback
982  return true;
983  },
984  // called when the first item is handled (eg, to implement playback)
985  onFirstMediaItem: function(aTargetList, aFirstMediaItem) { }
986  };
987 
988 To handle a drop with a specific mediaList target and drop insertion point, use
989 the following code:
990 
991  ExternalDropHandler.dropOnList(window,
992  dragSession,
993  targetMediaList,
994  targetMediaListPosition,
995  dropHandlerListener);
996 
997 In order to target the drop at the end of the targeted mediaList, you
998 should give a value of -1 for targetMediaListPosition.
999 
1000 Two similar methods (dropUrls and dropUrlsOnList) exist that let you simulate a
1001 drop by giving a list of URLs, and triggering the same handling as the one that
1002 would happen had these URLs been part of a dragsession drop.
1003 
1004 The other public methods in this helper can be used to simplify the rest of your
1005 drag and drop handler as well. For instance, an nsDragAndDrop observer's
1006 getSupportedFlavours() method may be implemented simply as:
1007 
1008  var flavours = new FlavourSet();
1009  ExternalDropHandler.addFlavours(flavours);
1010  return flavours;
1011 
1012 Also, getTransferData and DNDUtils.getTransferDataForFlavour may be used to
1013 inspect the content of the dragSession before deciding what to do with it.
1014 
1015 Finally, the standardReport and reportAddedTracks methods are used to send a
1016 temporary message on the status bar, to report the result of a drag and drop
1017 session. standardReport will format the text using the specific rules for what
1018 to show and in which circumstances, and reportAddedTracks will report exactly
1019 what you tell it to.
1020 
1021 Important note:
1022 ---------------
1023 
1024 The window being passed as a parameter to both the drop and dropOnList methods
1025 must implement the following two functions :
1026 
1027 SBOpenModalDialog(aChromeUrl, aTargetId, aWindowFeatures, aWindowArguments);
1028 installXPI(aXpiUrl);
1029 
1030 These two methods are respectively implemented in windowUtils.js and
1031 playerOpen.js, importing these scripts in your window ensures that the
1032 requirements are met.
1033 
1034 */
1035 
1036 var ExternalDropHandler = {
1037 
1038  supportedFlavours: [ "application/x-moz-file",
1039  "text/x-moz-url",
1040  "text/unicode"],
1041 
1042  // returns true if the drag session contains supported external flavors
1043  isSupported: function(aDragSession) {
1044  return DNDUtils.isSupported(aDragSession, this.supportedFlavours);
1045  },
1046 
1047  // performs a default drop of the drag session. media items go to the
1048  // main library.
1049  drop: function(aWindow, aDragSession, aListener) {
1050  var mainLibrary = Cc["@songbirdnest.com/Songbird/library/Manager;1"]
1051  .getService(Ci.sbILibraryManager)
1052  .mainLibrary;
1053  this.dropOnList(aWindow, aDragSession, mainLibrary, -1, aListener);
1054  },
1055 
1056  // performs a default drop of a list of urls. media items go to the
1057  // main library.
1058  dropUrls: function(aWindow, aUrlArray, aListener) {
1059  var mainLibrary = Cc["@songbirdnest.com/Songbird/library/Manager;1"]
1060  .getService(Ci.sbILibraryManager)
1061  .mainLibrary;
1062  this.dropUrlsOnList(aWindow, aUrlArray, mainLibrary, -1, aListener);
1063  },
1064 
1065  // perform a drop onto a medialist. media items are inserted at the specified
1066  // position in the list if that list is orderable. otherwise, or if the
1067  // position is invalid, the items are added to the target list.
1068  dropOnList: function(aWindow,
1069  aDragSession,
1070  aTargetList,
1071  aDropPosition,
1072  aListener) {
1073  if (!aTargetList) {
1074  throw new Error("No target medialist specified for dropOnList");
1075  }
1076  this._dropFiles(aWindow,
1077  aDragSession,
1078  null,
1079  aTargetList,
1080  aDropPosition,
1081  aListener);
1082  },
1083 
1084  // perform a drop of a list of urls onto a medialist. media items are inserted at
1085  // the specified position in the list if that list is orderable. otherwise, or if the
1086  // position is invalid, the items are added to the target list.
1087  dropUrlsOnList: function(aWindow,
1088  aUrlArray,
1089  aTargetList,
1090  aDropPosition,
1091  aListener) {
1092  if (!aTargetList) {
1093  throw new Error("No target medialist specified for dropOnList");
1094  }
1095  this._dropFiles(aWindow,
1096  null,
1097  aUrlArray,
1098  aTargetList,
1099  aDropPosition,
1100  aListener);
1101  },
1102 
1103  // call this to automatically add the supported external flavors
1104  // to a flavourSet object
1105  addFlavours: function(aFlavourSet) {
1106  DNDUtils.addFlavours(aFlavourSet, this.supportedFlavours);
1107  },
1108 
1109  // returns an array with the data for any supported external flavor
1110  getTransferData: function(aSession) {
1111  return DNDUtils.getTransferData(aSession, this.supportedFlavours);
1112  },
1113 
1114  // simply forward the call. here in this object for completeness
1115  // see DNDUtils.getTransferDataForFlavour for more info
1116  getTransferDataForFlavour: function(aFlavour, aSession, aArray) {
1117  return DNDUtils.getTransferDataForFlavour(aFlavour, aSession, aArray);
1118  },
1119 
1120  // --------------------------------------------------------------------------
1121  // methods below this point are pretend-private
1122  // --------------------------------------------------------------------------
1123 
1124  _listener : null, // listener object, for notifications
1125 
1126  // initiate the handling of all dropped files: this handling is sliced up
1127  // into a number of 'frames', each frame importing one item, or queuing up
1128  // one directory for later import. at the end of each frame, the
1129  // _nextImportDropFrame method is called to schedule the next frame using a
1130  // short timer, so as to give the UI time to catch up, and we keep doing that
1131  // until everything in the file queue has been processed. when that's done,
1132  // we then look for queued directory scans, which we give to the directory
1133  // import service. after the directories have been processed, we notify the
1134  // listener that processing has ended. Note that the function can take either
1135  // a drag session of an array of URLs. If both are provided, only the session
1136  // will be handled (ie, the method is not meant to be called with both a session
1137  // and a urlarray).
1138  _dropFiles: function(window, session, urlarray, targetlist, position, listener) {
1139 
1140  // check that we are indeed processing an external drop
1141  if (session && !this.isSupported(session)) {
1142  return;
1143  }
1144 
1145  // if we are on win32, we will need to make local filenames lowercase
1146  var lcase = (this._getPlatformString() == "Windows_NT");
1147 
1148  // get drop data in any of the supported formats
1149  var dropdata = session ? this.getTransferData(session) : urlarray;
1150 
1151  // reset first media item, so we know to record it again
1152  this._firstMediaItem = null;
1153 
1154  // remember listener
1155  this._listener = listener;
1156 
1157  // Install all the dropped XPI files at the same time
1158  var xpiArray = {};
1159  var xpiCount = 0;
1160 
1161  var uriList = Cc["@songbirdnest.com/moz/xpcom/threadsafe-array;1"]
1162  .createInstance(Ci.nsIMutableArray);
1163 
1164  var ioService = Cc["@mozilla.org/network/io-service;1"]
1165  .getService(Ci.nsIIOService);
1166 
1167  // process all entries in the drop
1168  for (var dropentry in dropdata) {
1169  var dropitem = dropdata[dropentry];
1170 
1171  var item, flavour;
1172  if (session) {
1173  item = dropitem[0];
1174  flavour = dropitem[2];
1175  } else {
1176  item = dropitem;
1177  flavour = "text/x-moz-url";
1178  }
1179  var islocal = true;
1180  var rawData;
1181 
1182  var prettyName;
1183 
1184  if (flavour == "application/x-moz-file") {
1185  var ioService = Cc["@mozilla.org/network/io-service;1"]
1186  .getService(Ci.nsIIOService);
1187  var fileHandler = ioService.getProtocolHandler("file")
1188  .QueryInterface(Ci.nsIFileProtocolHandler);
1189  rawData = fileHandler.getURLSpecFromFile(item);
1190 
1191  // Check to see that this is a xpi/jar - if so handle that event
1192  if ( /\.(xpi|jar)$/i.test(rawData) && (item instanceof Ci.nsIFile) ) {
1193  xpiArray[item.leafName] = {
1194  URL: rawData,
1195  IconURL: URI_GENERIC_ICON_XPINSTALL,
1196  toString: function() { return this.URL; }
1197  };
1198  ++xpiCount;
1199  }
1200  } else {
1201  if (item instanceof Ci.nsISupportsString) {
1202  rawData = item.toString();
1203  } else {
1204  rawData = ""+item;
1205  }
1206  if (rawData.toLowerCase().indexOf("http://") >= 0) {
1207  // remember that this is not a local file
1208  islocal = false;
1209  } else if (rawData.toLowerCase().indexOf("file://") >= 0) {
1210  islocal = true;
1211  } else {
1212  // not a url, ignore
1213  continue;
1214  }
1215  }
1216 
1217  // rawData contains a file or http URL to the dropped media.
1218 
1219  // check if there is a pretty name we can grab
1220  var separator = rawData.indexOf("\n");
1221  if (separator != -1) {
1222  prettyName = rawData.substr(separator+1);
1223  rawData = rawData.substr(0,separator);
1224  }
1225 
1226  // make filename lowercase if necessary (win32)
1227  if (lcase && islocal) {
1228  rawData = rawData.toLowerCase();
1229  }
1230 
1231  // record this file for later processing
1232  uriList.appendElement(ioService.newURI(rawData, null, null), false);
1233  }
1234 
1235  // Timeout the XPI install
1236  if (xpiCount > 0) {
1237  window.setTimeout(window.installXPIArray, 10, xpiArray);
1238  }
1239 
1240  var uriImportService = Cc["@songbirdnest.com/uri-import-service;1"]
1241  .getService(Ci.sbIURIImportService);
1242  uriImportService.importURIArray(uriList,
1243  window,
1244  targetlist,
1245  position,
1246  this);
1247  },
1248 
1249  // sbIURIImportListener
1250  onImportComplete: function(aTargetMediaList,
1251  aTotalImportCount,
1252  aTotalDupeCount,
1253  aTotalUnsupported,
1254  aTotalInserted,
1255  aOtherDrops)
1256  {
1257  var device,
1258  isDevice;
1259 
1260  // Get the device reference from the target library.
1261  try {
1262  device = aTargetMediaList.library.device;
1263  } catch (e) {
1264  device = null;
1265  }
1266 
1267  isDevice = (device !== null);
1268 
1269  if (this._listener) {
1270  if (this._listener.onDropComplete(aTargetMediaList,
1271  aTotalImportCount,
1272  aTotalDupeCount,
1273  aTotalInserted,
1274  aTotalUnsupported,
1275  aOtherDrops)) {
1276  DNDUtils.standardReport(aTargetMediaList,
1277  aTotalImportCount,
1278  aTotalDupeCount,
1279  aTotalUnsupported,
1280  aTotalInserted,
1281  aOtherDrops,
1282  isDevice);
1283  }
1284  }
1285  else {
1286  DNDUtils.standardReport(aTargetMediaList,
1287  aTotalImportCount,
1288  aTotalInserted, // usually dupes
1289  aTotalUnsupported,
1290  aTotalInserted,
1291  aOtherDrops,
1292  isDevice);
1293  }
1294  },
1295 
1296 
1297  onFirstMediaItem: function(aTargetMediaList, aTargetMediaItem)
1298  {
1299  if (this._listener) {
1300  this._listener.onFirstMediaItem(aTargetMediaList, aTargetMediaItem);
1301  }
1302  },
1303 
1304  // returns the platform string
1305  _getPlatformString: function() {
1306  try {
1307  var sysInfo =
1308  Components.classes["@mozilla.org/system-info;1"]
1309  .getService(Components.interfaces.nsIPropertyBag2);
1310  return sysInfo.getProperty("name");
1311  }
1312  catch (e) {
1313  var user_agent = navigator.userAgent;
1314  if (user_agent.indexOf("Windows") != -1)
1315  return "Windows_NT";
1316  else if (user_agent.indexOf("Mac OS X") != -1)
1317  return "Darwin";
1318  else if (user_agent.indexOf("Linux") != -1)
1319  return "Linux";
1320  else if (user_agent.indexOf("SunOS") != -1)
1321  return "SunOS";
1322  return "";
1323  }
1324  }
1325 
1326 }
1327 
1328 
1329 
1330 
function SBFormattedString(aKey, aParams, aDefault, aStringBundle)
const URI_GENERIC_ICON_XPINSTALL
Definition: DropHelper.jsm:57
EXPORTED_SYMBOLS
Definition: DropHelper.jsm:47
var DNDUtils
Definition: DropHelper.jsm:80
sbOSDControlService prototype QueryInterface
const TYPE_X_SB_TRANSFER_MEDIA_ITEMS
Definition: DropHelper.jsm:414
var ioService
let window
const SB_NewDataRemote
const SB_MEDIALISTDUPLICATEFILTER_CONTRACTID
Definition: DropHelper.jsm:69
const TYPE_X_SB_TRANSFER_MEDIA_LIST
Definition: DropHelper.jsm:413
var count
Definition: test_bug7406.js:32
const Cc
Definition: DropHelper.jsm:51
return null
Definition: FeedWriter.js:1143
SimpleArrayEnumerator prototype hasMoreElements
const Cu
Definition: DropHelper.jsm:55
oState session
const Ci
Definition: DropHelper.jsm:52
function msg
observe data
Definition: FeedWriter.js:1329
_getSelectedPageStyle s i
const Cr
Definition: DropHelper.jsm:53
_updateTextAndScrollDataForFrame aData
const Ce
Definition: DropHelper.jsm:54