summaryrefslogtreecommitdiffstats
path: root/comm/suite/components/downloads/DownloadsCommon.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'comm/suite/components/downloads/DownloadsCommon.jsm')
-rw-r--r--comm/suite/components/downloads/DownloadsCommon.jsm800
1 files changed, 800 insertions, 0 deletions
diff --git a/comm/suite/components/downloads/DownloadsCommon.jsm b/comm/suite/components/downloads/DownloadsCommon.jsm
new file mode 100644
index 0000000000..81fdcccfa3
--- /dev/null
+++ b/comm/suite/components/downloads/DownloadsCommon.jsm
@@ -0,0 +1,800 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var EXPORTED_SYMBOLS = [
+ "DownloadsCommon",
+];
+
+/**
+ * Handles the Downloads panel shared methods and data access.
+ *
+ * This file includes the following constructors and global objects:
+ *
+ * DownloadsCommon
+ * This object is exposed directly to the consumers of this JavaScript module,
+ * and provides shared methods for all the instances of the user interface.
+ *
+ * DownloadsData
+ * Retrieves the list of past and completed downloads from the underlying
+ * Downloads API data, and provides asynchronous notifications allowing
+ * to build a consistent view of the available data.
+ */
+
+// Globals
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } =
+ ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
+const { AppConstants } =
+ ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ NetUtil: "resource://gre/modules/NetUtil.jsm",
+ PluralForm: "resource://gre/modules/PluralForm.jsm",
+ DownloadHistory: "resource://gre/modules/DownloadHistory.jsm",
+ Downloads: "resource://gre/modules/Downloads.jsm",
+ DownloadUIHelper: "resource://gre/modules/DownloadUIHelper.jsm",
+ DownloadUtils: "resource://gre/modules/DownloadUtils.jsm",
+ OS: "resource://gre/modules/osfile.jsm",
+});
+
+XPCOMUtils.defineLazyGetter(this, "DownloadsLogger", () => {
+ let { ConsoleAPI } =
+ ChromeUtils.import("resource://gre/modules/Console.jsm", {});
+ let consoleOptions = {
+ maxLogLevelPref: "browser.download.loglevel",
+ prefix: "Downloads"
+ };
+ return new ConsoleAPI(consoleOptions);
+});
+
+const kDownloadsStringBundleUrl =
+ "chrome://communicator/locale/downloads/downloadmanager.properties";
+
+// Currently not used. Keep for future updates.
+const kDownloadsStringsRequiringFormatting = {
+ fileExecutableSecurityWarning: true
+};
+
+// Currently not used. Keep for future updates.
+const kDownloadsStringsRequiringPluralForm = {
+ otherDownloads3: true
+};
+
+const kPartialDownloadSuffix = ".part";
+
+const kPrefBranch = Services.prefs.getBranch("browser.download.");
+
+const PREF_DM_BEHAVIOR = "browser.download.manager.behavior";
+const PROGRESS_DIALOG_URL = "chrome://communicator/content/downloads/progressDialog.xul";
+
+var PrefObserver = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+ getPref(name) {
+ try {
+ switch (typeof this.prefs[name]) {
+ case "boolean":
+ return kPrefBranch.getBoolPref(name);
+ }
+ } catch (ex) { }
+ return this.prefs[name];
+ },
+ observe(aSubject, aTopic, aData) {
+ if (this.prefs.hasOwnProperty(aData)) {
+ delete this[aData];
+ this[aData] = this.getPref(aData);
+ }
+ },
+ register(prefs) {
+ this.prefs = prefs;
+ kPrefBranch.addObserver("", this, true);
+ for (let key in prefs) {
+ let name = key;
+ XPCOMUtils.defineLazyGetter(this, name, function() {
+ return PrefObserver.getPref(name);
+ });
+ }
+ },
+};
+
+// PrefObserver.register({
+ // prefName: defaultValue
+// });
+
+
+// DownloadsCommon
+
+/**
+ * This object is exposed directly to the consumers of this JavaScript module,
+ * and provides shared methods for all the instances of the user interface.
+ */
+var DownloadsCommon = {
+ // The following legacy constants are still returned by stateOfDownload, but
+ // individual properties of the Download object should normally be used.
+ DOWNLOAD_NOTSTARTED: -1,
+ DOWNLOAD_DOWNLOADING: 0,
+ DOWNLOAD_FINISHED: 1,
+ DOWNLOAD_FAILED: 2,
+ DOWNLOAD_CANCELED: 3,
+ DOWNLOAD_PAUSED: 4,
+ DOWNLOAD_BLOCKED_PARENTAL: 6,
+ DOWNLOAD_DIRTY: 8,
+ DOWNLOAD_BLOCKED_POLICY: 9,
+
+ // The following are the possible values of the "attention" property.
+ ATTENTION_NONE: "",
+ ATTENTION_SUCCESS: "success",
+ ATTENTION_WARNING: "warning",
+ ATTENTION_SEVERE: "severe",
+
+ /**
+ * Returns an object whose keys are the string names from the downloads string
+ * bundle, and whose values are either the translated strings or functions
+ * returning formatted strings.
+ */
+ get strings() {
+ let strings = {};
+ let sb = Services.strings.createBundle(kDownloadsStringBundleUrl);
+ let enumerator = sb.getSimpleEnumeration();
+ while (enumerator.hasMoreElements()) {
+ let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
+ let stringName = string.key;
+ if (stringName in kDownloadsStringsRequiringFormatting) {
+ strings[stringName] = function() {
+ // Convert "arguments" to a real array before calling into XPCOM.
+ return sb.formatStringFromName(stringName,
+ Array.slice(arguments, 0),
+ arguments.length);
+ };
+ } else if (stringName in kDownloadsStringsRequiringPluralForm) {
+ strings[stringName] = function(aCount) {
+ // Convert "arguments" to a real array before calling into XPCOM.
+ let formattedString = sb.formatStringFromName(stringName,
+ Array.slice(arguments, 0),
+ arguments.length);
+ return PluralForm.get(aCount, formattedString);
+ };
+ } else {
+ strings[stringName] = string.value;
+ }
+ }
+ delete this.strings;
+ return this.strings = strings;
+ },
+
+ /**
+ * Get access to one of the DownloadsData or HistoryDownloadsData objects
+ * depending on whether history downloads should be included.
+ *
+ * @param window
+ * The browser window which owns the download button.
+ * @param [optional] history
+ * True to include history downloads when the window is public.
+ */
+ // does not apply in SM
+ getData(window, history = false) {
+ if (history) {
+ return HistoryDownloadsData;
+ }
+ return DownloadsData;
+ },
+
+ /**
+ * Initializes the Downloads Manager common code.
+ */
+ init() {
+ const { DownloadsData } =
+ ChromeUtils.import("resource://gre/modules/Downloads.jsm");
+ const { DownloadIntegration } =
+ ChromeUtils.import("resource://gre/modules/DownloadIntegration.jsm");
+ DownloadIntegration.shouldPersistDownload = function() { return true; };
+ DownloadsData.initializeDataLink();
+ },
+
+ /**
+ * Returns the legacy state integer value for the provided Download object.
+ */
+ stateOfDownload(download) {
+ // Collapse state using the correct priority.
+ if (!download.stopped) {
+ return DownloadsCommon.DOWNLOAD_DOWNLOADING;
+ }
+ if (download.succeeded) {
+ return DownloadsCommon.DOWNLOAD_FINISHED;
+ }
+ if (download.error) {
+ if (download.error.becauseBlockedByParentalControls) {
+ return DownloadsCommon.DOWNLOAD_BLOCKED_PARENTAL;
+ }
+ if (download.error.becauseBlockedByReputationCheck) {
+ return DownloadsCommon.DOWNLOAD_DIRTY;
+ }
+ return DownloadsCommon.DOWNLOAD_FAILED;
+ }
+ if (download.canceled) {
+ if (download.hasPartialData) {
+ return DownloadsCommon.DOWNLOAD_PAUSED;
+ }
+ return DownloadsCommon.DOWNLOAD_CANCELED;
+ }
+ return DownloadsCommon.DOWNLOAD_NOTSTARTED;
+ },
+
+ /**
+ * Returns the state as a string for the provided Download object.
+ */
+ stateOfDownloadText(download) {
+ // Don't duplicate the logic so just call stateOfDownload.
+ let state = this.stateOfDownload(download);
+ let s = DownloadsCommon.strings;
+ let title = s.unblockHeaderUnblock;
+ let verboseState;
+
+ switch (state) {
+ case DownloadsCommon.DOWNLOAD_PAUSED:
+ verboseState = s.statePaused;
+ break;
+ case DownloadsCommon.DOWNLOAD_DOWNLOADING:
+ verboseState = s.stateDownloading;
+ break;
+ case DownloadsCommon.DOWNLOAD_FINISHED:
+ verboseState = s.stateCompleted;
+ break;
+ case DownloadsCommon.DOWNLOAD_FAILED:
+ verboseState = s.stateFailed;
+ break;
+ case DownloadsCommon.DOWNLOAD_CANCELED:
+ verboseState = s.stateCanceled;
+ break;
+ // Security Zone Policy
+ case DownloadsCommon.DOWNLOAD_BLOCKED_PARENTAL:
+ // Security Zone Policy
+ verboseState = s.stateBlockedParentalControls;
+ break;
+ // Security Zone Policy
+ case DownloadsCommon.DOWNLOAD_BLOCKED_POLICY:
+ verboseState = s.stateBlockedPolicy;
+ break;
+ // possible virus/spyware
+ case DownloadsCommon.DOWNLOAD_DIRTY:
+ verboseState = s.stateDirty;
+ break;
+ // Currently not returned.
+ case DownloadsCommon.DOWNLOAD_UPLOADING:
+ verboseState = s.stateNotStarted;
+ break;
+ case DownloadsCommon.DOWNLOAD_NOTSTARTED:
+ verboseState = s.stateNotStarted;
+ break;
+ // Whoops!
+ default:
+ verboseState = s.stateUnknown;
+ break;
+ }
+
+ return verboseState;
+ },
+
+ /**
+ * Returns the transfer progress text for the provided Download object.
+ */
+ getTransferredBytes(download) {
+ let currentBytes;
+ let totalBytes;
+ // Download in progress.
+ // Download paused / canceled and has partial data.
+ if (!download.stopped ||
+ (download.canceled && download.hasPartialData)) {
+ currentBytes = download.currentBytes,
+ totalBytes = download.hasProgress ? download.totalBytes : -1;
+ // Download done but file missing.
+ } else if (download.succeeded && !download.exists) {
+ currentBytes = download.totalBytes ? download.totalBytes : -1;
+ totalBytes = -1;
+ // For completed downloads, show the file size
+ } else if (download.succeeded && download.target.size !== undefined) {
+ currentBytes = download.target.size;
+ totalBytes = -1;
+ // Some local files saves e.g. from attachments also have no size.
+ // They only have a target in downloads.json but no target.path.
+ // FIX ME later.
+ } else {
+ currentBytes = -1;
+ totalBytes = -1;
+ }
+
+ // We do not want to show 0 of xxx bytes.
+ if (currentBytes == 0) {
+ currentBytes = -1;
+ }
+
+ if (totalBytes == 0) {
+ totalBytes = -1;
+ }
+
+ // We tried everything.
+ if (currentBytes == -1 && totalBytes == -1) {
+ return "";
+ }
+
+ return DownloadUtils.getTransferTotal(currentBytes, totalBytes);
+ },
+
+ /**
+ * Returns the time remaining text for the provided Download object.
+ * For calculation a variable is stored in it.
+ */
+ getTimeRemaining(download) {
+ // If you do changes here please check progressDialog.js.
+ if (!download.stopped) {
+ let lastSec = (download.lastSec == null) ? Infinity : download.lastSec;
+ // Calculate the time remaining if we have valid values
+ let seconds = (download.speed > 0) && (download.totalBytes > 0)
+ ? (download.totalBytes - download.currentBytes) / download.speed
+ : -1;
+ let [timeLeft, newLast] = DownloadUtils.getTimeLeft(seconds, lastSec);
+ // Store it back for next calculation.
+ download.lastSec = newLast;
+ return timeLeft;
+ }
+ return "";
+ },
+
+ /**
+ * Opens a downloaded file.
+ *
+ * @param aFile
+ * the downloaded file to be opened.
+ * @param aMimeInfo
+ * the mime type info object. May be null.
+ * @param aOwnerWindow
+ * the window with which this action is associated.
+ */
+ openDownloadedFile(aFile, aMimeInfo, aOwnerWindow) {
+ if (!(aFile instanceof Ci.nsIFile)) {
+ throw new Error("aFile must be a nsIFile object");
+ }
+ if (aMimeInfo && !(aMimeInfo instanceof Ci.nsIMIMEInfo)) {
+ throw new Error("Invalid value passed for aMimeInfo");
+ }
+ if (!(aOwnerWindow instanceof Ci.nsIDOMWindow)) {
+ throw new Error("aOwnerWindow must be a dom-window object");
+ }
+
+ let isWindowsExe = AppConstants.platform == "win" &&
+ aFile.leafName.toLowerCase().endsWith(".exe");
+
+ let promiseShouldLaunch;
+ // Don't prompt on Windows for .exe since there will be a native prompt.
+ if (aFile.isExecutable() && !isWindowsExe) {
+ // We get a prompter for the provided window here, even though anchoring
+ // to the most recently active window should work as well.
+ promiseShouldLaunch =
+ DownloadUIHelper.getPrompter(aOwnerWindow)
+ .confirmLaunchExecutable(aFile.path);
+ } else {
+ promiseShouldLaunch = Promise.resolve(true);
+ }
+
+ promiseShouldLaunch.then(shouldLaunch => {
+ if (!shouldLaunch) {
+ return;
+ }
+
+ // Actually open the file.
+ try {
+ if (aMimeInfo && aMimeInfo.preferredAction == aMimeInfo.useHelperApp) {
+ aMimeInfo.launchWithFile(aFile);
+ return;
+ }
+ } catch (ex) { }
+
+ // If either we don't have the mime info, or the preferred action failed,
+ // attempt to launch the file directly.
+ try {
+ aFile.launch();
+ } catch (ex) {
+ // If launch fails, try sending it through the system's external "file:"
+ // URL handler.
+ Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService)
+ .loadUrl(NetUtil.newURI(aFile));
+ }
+ }).catch(Cu.reportError);
+ },
+
+ /**
+ * Show a downloaded file in the system file manager.
+ *
+ * @param aFile
+ * a downloaded file.
+ */
+ showDownloadedFile(aFile) {
+ if (!(aFile instanceof Ci.nsIFile)) {
+ throw new Error("aFile must be a nsIFile object");
+ }
+ try {
+ // Show the directory containing the file and select the file.
+ aFile.reveal();
+ } catch (ex) {
+ // If reveal fails for some reason (e.g., it's not implemented on unix
+ // or the file doesn't exist), try using the parent if we have it.
+ let parent = aFile.parent;
+ if (parent) {
+ this.showDirectory(parent);
+ }
+ }
+ },
+
+ /**
+ * Show the specified folder in the system file manager.
+ *
+ * @param aDirectory
+ * a directory to be opened with system file manager.
+ */
+ showDirectory(aDirectory) {
+ if (!(aDirectory instanceof Ci.nsIFile)) {
+ throw new Error("aDirectory must be a nsIFile object");
+ }
+ try {
+ aDirectory.launch();
+ } catch (ex) {
+ // If launch fails (probably because it's not implemented), let
+ // the OS handler try to open the directory.
+ Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService)
+ .loadUrl(NetUtil.newURI(aDirectory));
+ }
+ },
+
+ /**
+ * Displays an alert message box which asks the user if they want to
+ * unblock the downloaded file or not.
+ *
+ * @param options
+ * An object with the following properties:
+ * {
+ * verdict:
+ * The detailed reason why the download was blocked, according to
+ * the "Downloads.Error.BLOCK_VERDICT_" constants. If an unknown
+ * reason is specified, "Downloads.Error.BLOCK_VERDICT_MALWARE" is
+ * assumed.
+ * window:
+ * The window with which this action is associated.
+ * dialogType:
+ * String that determines which actions are available:
+ * - "unblock" to offer just "unblock".
+ * - "chooseUnblock" to offer "unblock" and "confirmBlock".
+ * - "chooseOpen" to offer "open" and "confirmBlock".
+ * }
+ *
+ * @return {Promise}
+ * @resolves String representing the action that should be executed:
+ * - "open" to allow the download and open the file.
+ * - "unblock" to allow the download without opening the file.
+ * - "confirmBlock" to delete the blocked data permanently.
+ * - "cancel" to do nothing and cancel the operation.
+ */
+ async confirmUnblockDownload({ verdict, window,
+ dialogType }) {
+ let s = DownloadsCommon.strings;
+
+ // All the dialogs have an action button and a cancel button, while only
+ // some of them have an additonal button to remove the file. The cancel
+ // button must always be the one at BUTTON_POS_1 because this is the value
+ // returned by confirmEx when using ESC or closing the dialog (bug 345067).
+ let title = s.unblockHeaderUnblock;
+ let firstButtonText = s.unblockButtonUnblock;
+ let firstButtonAction = "unblock";
+ let buttonFlags =
+ (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) +
+ (Ci.nsIPrompt.BUTTON_TITLE_CANCEL * Ci.nsIPrompt.BUTTON_POS_1);
+
+ switch (dialogType) {
+ case "unblock":
+ // Use only the unblock action. The default is to cancel.
+ buttonFlags += Ci.nsIPrompt.BUTTON_POS_1_DEFAULT;
+ break;
+ case "chooseUnblock":
+ // Use the unblock and remove file actions. The default is remove file.
+ buttonFlags +=
+ (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2) +
+ Ci.nsIPrompt.BUTTON_POS_2_DEFAULT;
+ break;
+ case "chooseOpen":
+ // Use the unblock and open file actions. The default is open file.
+ title = s.unblockHeaderOpen;
+ firstButtonText = s.unblockButtonOpen;
+ firstButtonAction = "open";
+ buttonFlags +=
+ (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2) +
+ Ci.nsIPrompt.BUTTON_POS_0_DEFAULT;
+ break;
+ default:
+ Cu.reportError("Unexpected dialog type: " + dialogType);
+ return "cancel";
+ }
+
+ let message;
+ switch (verdict) {
+ case Downloads.Error.BLOCK_VERDICT_UNCOMMON:
+ message = s.unblockTypeUncommon2;
+ break;
+ case Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED:
+ message = s.unblockTypePotentiallyUnwanted2;
+ break;
+ default: // Assume Downloads.Error.BLOCK_VERDICT_MALWARE
+ message = s.unblockTypeMalware;
+ break;
+ }
+ message += "\n\n" + s.unblockTip2;
+
+ Services.ww.registerNotification(function onOpen(subj, topic) {
+ if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) {
+ // Make sure to listen for "DOMContentLoaded" because it is fired
+ // before the "load" event.
+ subj.addEventListener("DOMContentLoaded", function() {
+ if (subj.document.documentURI ==
+ "chrome://global/content/commonDialog.xul") {
+ Services.ww.unregisterNotification(onOpen);
+ let dialog = subj.document.getElementById("commonDialog");
+ if (dialog) {
+ // Change the dialog to use a warning icon.
+ dialog.classList.add("alert-dialog");
+ }
+ }
+ }, {once: true});
+ }
+ });
+
+ let rv = Services.prompt.confirmEx(window, title, message, buttonFlags,
+ firstButtonText, null,
+ s.unblockButtonConfirmBlock, null, {});
+ return [firstButtonAction, "cancel", "confirmBlock"][rv];
+ },
+};
+
+XPCOMUtils.defineLazyGetter(this.DownloadsCommon, "log", () => {
+ return DownloadsLogger.log.bind(DownloadsLogger);
+});
+XPCOMUtils.defineLazyGetter(this.DownloadsCommon, "error", () => {
+ return DownloadsLogger.error.bind(DownloadsLogger);
+});
+
+// DownloadsData
+
+/**
+ * Retrieves the list of past and completed downloads from the underlying
+ * Downloads API data, and provides asynchronous notifications allowing to
+ * build a consistent view of the available data.
+ *
+ * Note that using this object does not automatically initialize the list of
+ * downloads. This is useful to display a neutral progress indicator in
+ * the main browser window until the autostart timeout elapses.
+ *
+ * This powers the DownloadsData and HistoryDownloadsData singleton objects.
+ */
+ function DownloadsDataCtor({ isHistory } = {}) {
+
+ // Contains all the available Download objects and their integer state.
+ this.oldDownloadStates = new Map();
+
+ // For the history downloads list we don't need to register this as a view,
+ // but we have to ensure that the DownloadsData object is initialized before
+ // we register more views. This ensures that the view methods of DownloadsData
+ // are invoked before those of views registered on HistoryDownloadsData,
+ // allowing the endTime property to be set correctly.
+ if (isHistory) {
+ DownloadsData.initializeDataLink();
+ this._promiseList = DownloadsData._promiseList
+ .then(() => DownloadHistory.getList());
+ return;
+ }
+
+ // This defines "initializeDataLink" and "_promiseList" synchronously, then
+ // continues execution only when "initializeDataLink" is called, allowing the
+ // underlying data to be loaded only when actually needed.
+ this._promiseList = (async () => {
+ await new Promise(resolve => this.initializeDataLink = resolve);
+ let list = await Downloads.getList(Downloads.ALL);
+
+ await list.addView(this);
+ this._downloadsLoaded = true;
+
+ return list;
+ })();
+}
+
+DownloadsDataCtor.prototype = {
+ /**
+ * Starts receiving events for current downloads.
+ */
+ initializeDataLink() {},
+
+ /**
+ * Used by sound logic when download ends.
+ */
+ _sound: null,
+ /**
+ * Promise resolved with the underlying DownloadList object once we started
+ * receiving events for current downloads.
+ */
+ _promiseList: null,
+
+ _downloadsLoaded: null,
+
+ /**
+ * Iterator for all the available Download objects. This is empty until the
+ * data has been loaded using the JavaScript API for downloads.
+ */
+ get downloads() {
+ return this.oldDownloadStates.keys();
+ },
+
+ /**
+ * True if there are finished downloads that can be removed from the list.
+ */
+ get canRemoveFinished() {
+ for (let download of this.downloads) {
+ // Stopped, paused, and failed downloads with partial data are removed.
+ if (download.stopped && !(download.canceled && download.hasPartialData)) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Asks the back-end to remove finished downloads from the list. This method
+ * is only called after the data link has been initialized.
+ */
+ removeFinished() {
+ Downloads.getList(Downloads.ALL)
+ .then(list => list.removeFinished())
+ .catch(Cu.reportError);
+ },
+
+ // Integration with the asynchronous Downloads back-end
+
+ // Download view
+ onDownloadAdded: function(download)
+ {
+ // Download objects do not store the end time of downloads, as the Downloads
+ // API does not need to persist this information for all platforms. Once a
+ // download terminates on a Desktop browser, it becomes a history download,
+ // for which the end time is stored differently, as a Places annotation.
+ download.endTime = Date.now();
+ this.oldDownloadStates.set(download,
+ DownloadsCommon.stateOfDownload(download));
+
+ download.displayName =
+ download.target.path ? OS.Path.basename(download.target.path)
+ : download.source.url;
+ this.onDownloadChanged(download);
+ if (!this._downloadsLoaded)
+ return;
+
+ var behavior = download.source.isPrivate ? 1 :
+ Services.prefs.getIntPref(PREF_DM_BEHAVIOR);
+ switch (behavior) {
+ case 0:
+ Cc["@mozilla.org/suite/suiteglue;1"]
+ .getService(Ci.nsISuiteGlue)
+ .showDownloadManager(true);
+ break;
+ case 1:
+ Services.ww.openWindow(null, PROGRESS_DIALOG_URL, null,
+ "chrome,titlebar,centerscreen,minimizable=yes,dialog=no",
+ { wrappedJSObject: download });
+ break;
+ }
+
+ return; // No UI for behavior >= 2
+ },
+
+ onDownloadChanged(download) {
+ let oldState = this.oldDownloadStates.get(download);
+ let newState = DownloadsCommon.stateOfDownload(download);
+ this.oldDownloadStates.set(download, newState);
+
+ if (oldState != newState &&
+ (download.succeeded ||
+ (download.canceled && !download.hasPartialData) ||
+ download.error)) {
+ // Store the end time that may be displayed by the views.
+ download.endTime = Date.now();
+
+ // This state transition code should actually be located in a Downloads
+ // API module (bug 941009).
+ // This might end with an exception if it is an unsupported uri scheme.
+ DownloadHistory.updateMetaData(download);
+
+ if (download.succeeded) {
+ this.playDownloadSound();
+ }
+ }
+ },
+
+ onDownloadRemoved(download) {
+ this.oldDownloadStates.delete(download);
+ },
+
+ // Download summary
+ onSummaryChanged: function() {
+
+ if (!gTaskbarProgress)
+ return;
+
+ const nsITaskbarProgress = Ci.nsITaskbarProgress;
+ var currentBytes = gDownloadsSummary.progressCurrentBytes;
+ var totalBytes = gDownloadsSummary.progressTotalBytes;
+ var state = gDownloadsSummary.allHaveStopped ?
+ currentBytes ? nsITaskbarProgress.STATE_PAUSED :
+ nsITaskbarProgress.STATE_NO_PROGRESS :
+ currentBytes < totalBytes ? nsITaskbarProgress.STATE_NORMAL :
+ nsITaskbarProgress.STATE_INDETERMINATE;
+ switch (state) {
+ case nsITaskbarProgress.STATE_NO_PROGRESS:
+ case nsITaskbarProgress.STATE_INDETERMINATE:
+ gTaskbarProgress.setProgressState(state, 0, 0);
+ break;
+ default:
+ gTaskbarProgress.setProgressState(state, currentBytes, totalBytes);
+ break;
+ }
+ },
+
+ // Play a download sound.
+ playDownloadSound: function()
+ {
+ if (Services.prefs.getBoolPref("browser.download.finished_download_sound")) {
+ if (!this._sound)
+ this._sound = Cc["@mozilla.org/sound;1"].createInstance(Ci.nsISound);
+ try {
+ let url = Services.prefs.getStringPref("browser.download.finished_sound_url");
+ this._sound.play(Services.io.newURI(url));
+ } catch (e) {
+ this._sound.beep();
+ }
+ }
+ },
+
+ // Registration of views
+
+ /**
+ * Adds an object to be notified when the available download data changes.
+ * The specified object is initialized with the currently available downloads.
+ *
+ * @param aView
+ * DownloadsView object to be added. This reference must be passed to
+ * removeView before termination.
+ */
+ addView(aView) {
+ this._promiseList.then(list => list.addView(aView))
+ .catch(Cu.reportError);
+ },
+
+ /**
+ * Removes an object previously added using addView.
+ *
+ * @param aView
+ * DownloadsView object to be removed.
+ */
+ removeView(aView) {
+ this._promiseList.then(list => list.removeView(aView))
+ .catch(Cu.reportError);
+ },
+};
+
+XPCOMUtils.defineLazyGetter(this, "HistoryDownloadsData", function() {
+ return new DownloadsDataCtor({ isHistory: true });
+});
+
+XPCOMUtils.defineLazyGetter(this, "DownloadsData", function() {
+ return new DownloadsDataCtor();
+});