// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- /* 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/. */ const URI_GENERIC_ICON_DOWNLOAD = "chrome://browser/skin/images/alert-downloads-30.png"; var Downloads = { _inited: false, _progressAlert: null, get manager() { return Cc["@mozilla.org/download-manager;1"] .getService(Ci.nsIDownloadManager); }, _getReferrerOrSource: function dh__getReferrerOrSource(aDownload) { return aDownload.referrer.spec || aDownload.source.spec; }, _getLocalFile: function dh__getLocalFile(aFileURI) { // XXX it's possible that using a null char-set here is bad const fileUrl = Services.io.newURI(aFileURI.spec || aFileURI, null, null).QueryInterface(Ci.nsIFileURL); return fileUrl.file.clone().QueryInterface(Ci.nsILocalFile); }, init: function dh_init() { if (this._inited) return; this._inited = true; Services.obs.addObserver(this, "dl-start", true); Services.obs.addObserver(this, "dl-done", true); }, uninit: function dh_uninit() { Services.obs.removeObserver(this, "dl-start"); Services.obs.removeObserver(this, "dl-done"); }, openDownload: function dh_openDownload(aDownload) { // expects xul item let id = aDownload.getAttribute("downloadId"); let download = this.manager.getDownload(id) let fileURI = download.target; let file = this._getLocalFile(fileURI); try { file.launch(); } catch (ex) { } }, removeDownload: function dh_removeDownload(aDownload) { // aDownload is the XUL element here, // and .target currently returns the target attribute (string value) let id = aDownload.getAttribute("downloadId"); let download = this.manager.getDownload(id); if (download) { // TODO add class/mark element for removal transition? this.manager.removeDownload(id); } }, cancelDownload: function dh_cancelDownload(aDownload) { let id = aDownload.getAttribute("downloadId"); let download = this.manager.getDownload(id) this.manager.cancelDownload(id); let fileURI = download.target; let file = this._getLocalFile(fileURI); if (file.exists()) file.remove(false); }, pauseDownload: function dh_pauseDownload(aDownload) { let id = aDownload.getAttribute("downloadId"); this.manager.pauseDownload(id); }, resumeDownload: function dh_resumeDownload(aDownload) { let id = aDownload.getAttribute("downloadId"); this.manager.resumeDownload(id); }, retryDownload: function dh_retryDownload(aDownload) { let id = aDownload.getAttribute("downloadId"); this.manager.retryDownload(id); }, showPage: function dh_showPage(aDownload) { let id = aDownload.getAttribute("downloadId"); let download = this.manager.getDownload(id) let uri = this._getReferrerOrSource(download); if (uri) BrowserUI.newTab(uri, Browser.selectedTab); }, showAlert: function dh_showAlert(aName, aMessage, aTitle, aIcon) { var notifier = Cc["@mozilla.org/alerts-service;1"] .getService(Ci.nsIAlertsService); // Callback for tapping on the alert popup let observer = { observe: function (aSubject, aTopic, aData) { if (aTopic == "alertclickcallback") PanelUI.show("downloads-container"); } }; if (!aTitle) aTitle = Strings.browser.GetStringFromName("alertDownloads"); if (!aIcon) aIcon = URI_GENERIC_ICON_DOWNLOAD; notifier.showAlertNotification(aIcon, aTitle, aMessage, true, "", observer, aName); }, observe: function (aSubject, aTopic, aData) { let download = aSubject.QueryInterface(Ci.nsIDownload); let msgKey = ""; switch (aTopic) { case "dl-start": msgKey = "alertDownloadsStart"; if (!this._progressAlert) { this._progressAlert = new AlertDownloadProgressListener(); this.manager.addListener(this._progressAlert); } break; case "dl-done": msgKey = "alertDownloadsDone"; break; } if (msgKey) { let message = Strings.browser.formatStringFromName(msgKey, [download.displayName], 1); let url = download.target.spec.replace("file:", "download:"); this.showAlert(url, message); } }, QueryInterface: function (aIID) { if (!aIID.equals(Ci.nsIObserver) && !aIID.equals(Ci.nsISupportsWeakReference) && !aIID.equals(Ci.nsISupports)) throw Components.results.NS_ERROR_NO_INTERFACE; return this; } }; /** * Wraps a list/grid control implementing nsIDOMXULSelectControlElement and * fills it with the user's downloads. It observes download notifications, * so it'll automatically update to reflect current download progress. * * @param aSet Control implementing nsIDOMXULSelectControlElement. * @param {Number} aLimit Maximum number of items to show in the view. */ function DownloadsView(aSet, aLimit) { this._set = aSet; this._limit = aLimit; this._progress = new DownloadProgressListener(this); Downloads.manager.addListener(this._progress); // Look for the removed download notification let obs = Cc["@mozilla.org/observer-service;1"]. getService(Ci.nsIObserverService); obs.addObserver(this, "download-manager-remove-download-guid", false); this.getDownloads(); } DownloadsView.prototype = { _progress: null, _stmt: null, _timeoutID: null, _set: null, _limit: null, _getItemForDownloadGuid: function dv__getItemForDownload(aGuid) { return this._set.querySelector("richgriditem[downloadGuid='" + aGuid + "']"); }, _getItemForDownload: function dv__getItemForDownload(aDownload) { return this._set.querySelector("richgriditem[downloadId='" + aDownload.id + "']"); }, _getDownloadForItem: function dv__getDownloadForItem(anItem) { let id = anItem.getAttribute("downloadId"); return Downloads.manager.getDownload(id); }, _getAttrsForDownload: function dv__getAttrsForDownload(aDownload) { // expects a DownloadManager download object return { typeName: 'download', downloadId: aDownload.id, downloadGuid: aDownload.guid, name: aDownload.displayName, target: aDownload.target, iconURI: "moz-icon://" + aDownload.displayName + "?size=64", date: DownloadUtils.getReadableDates(new Date())[0], domain: DownloadUtils.getURIHost(aDownload.source.spec)[0], size: this._getDownloadSize(aDownload.size), state: aDownload.state }; }, _updateItemWithAttrs: function dv__updateItemWithAttrs(anItem, aAttrs) { for (let name in aAttrs) anItem.setAttribute(name, aAttrs[name]); }, _getDownloadSize: function dv__getDownloadSize (aSize) { let displaySize = DownloadUtils.convertByteUnits(aSize); if (displaySize[0] > 0) // [0] is size, [1] is units return displaySize.join(""); else return Strings.browser.GetStringFromName("downloadsUnknownSize"); }, _initStatement: function dv__initStatement() { if (this._stmt) this._stmt.finalize(); let limitClause = this._limit ? ("LIMIT " + this._limit) : ""; this._stmt = Downloads.manager.DBConnection.createStatement( "SELECT id, guid, name, target, source, state, startTime, endTime, referrer, " + "currBytes, maxBytes, state IN (?1, ?2, ?3, ?4, ?5) isActive " + "FROM moz_downloads " + "ORDER BY isActive DESC, endTime DESC, startTime DESC " + limitClause); }, _stepDownloads: function dv__stepDownloads(aNumItems) { try { if (!this._stmt.executeStep()) { // final record; this._stmt.finalize(); this._stmt = null; this._fire("DownloadsReady", this._set); return; } let attrs = { typeName: 'download', // TODO: the remove event gives us guid not id; should we use guid as the download (record) id? downloadGuid: this._stmt.row.guid, downloadId: this._stmt.row.id, name: this._stmt.row.name, target: this._stmt.row.target, iconURI: "moz-icon://" + this._stmt.row.name + "?size=25", date: DownloadUtils.getReadableDates(new Date(this._stmt.row.endTime / 1000))[0], domain: DownloadUtils.getURIHost(this._stmt.row.source)[0], size: this._getDownloadSize(this._stmt.row.maxBytes), state: this._stmt.row.state }; let item = this._set.appendItem(attrs.target, attrs.downloadId); this._updateItemWithAttrs(item, attrs); } catch (e) { // Something went wrong when stepping or getting values, so quit this._stmt.reset(); return; } // Add another item to the list if we should; // otherwise, let the UI update and continue later if (aNumItems > 1) { this._stepDownloads(aNumItems - 1); } else { // Use a shorter delay for earlier downloads to display them faster let delay = Math.min(this._set.itemCount * 10, 300); let self = this; this._timeoutID = setTimeout(function() { self._stepDownloads(5); }, delay); } }, _fire: function _fire(aName, anElement) { let event = document.createEvent("Events"); event.initEvent(aName, true, true); anElement.dispatchEvent(event); }, observe: function dv_managerObserver(aSubject, aTopic, aData) { // observer-service message handler switch (aTopic) { case "download-manager-remove-download-guid": let guid = aSubject.QueryInterface(Ci.nsISupportsCString); this.removeDownload({ guid: guid }); return; } }, getDownloads: function dv_getDownloads() { this._initStatement(); clearTimeout(this._timeoutID); // Since we're pulling in all downloads, clear the list to avoid duplication this.clearDownloads(); this._stmt.reset(); this._stmt.bindInt32Parameter(0, Ci.nsIDownloadManager.DOWNLOAD_NOTSTARTED); this._stmt.bindInt32Parameter(1, Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING); this._stmt.bindInt32Parameter(2, Ci.nsIDownloadManager.DOWNLOAD_PAUSED); this._stmt.bindInt32Parameter(3, Ci.nsIDownloadManager.DOWNLOAD_QUEUED); this._stmt.bindInt32Parameter(4, Ci.nsIDownloadManager.DOWNLOAD_SCANNING); // Take a quick break before we actually start building the list let self = this; this._timeoutID = setTimeout(function() { self._stepDownloads(1); }, 0); }, clearDownloads: function dv_clearDownloads() { while (this._set.itemCount > 0) this._set.removeItemAt(0); }, addDownload: function dv_addDownload(aDownload) { // expects a download manager download object let attrs = this._getAttrsForDownload(aDownload); let item = this._set.insertItemAt(0, attrs.target, attrs.downloadId); this._updateItemWithAttrs(item, attrs); }, updateDownload: function dv_updateDownload(aDownload) { // expects a download manager download object let item = this._getItemForDownload(aDownload); if (!item) return; let attrs = this._getAttrsForDownload(aDownload); this._updateItemWithAttrs(item, attrs); }, removeDownload: function dv_removeDownload(aDownload) { // expects an object with id or guid property let item; if (aDownload.id) { item = this._getItemForDownload(aDownload.id); } else if (aDownload.guid) { item = this._getItemForDownloadGuid(aDownload.guid); } if (!item) return; let idx = this._set.getIndexOfItem(item); if (idx < 0) return; // any transition needed here? this._set.removeItemAt(idx); }, destruct: function dv_destruct() { Downloads.manager.removeListener(this._progress); } }; var DownloadsPanelView = { _view: null, get _grid() { return document.getElementById("downloads-list"); }, get visible() { return PanelUI.isPaneVisible("downloads-container"); }, init: function init() { this._view = new DownloadsView(this._grid); }, show: function show() { this._grid.arrangeItems(); }, uninit: function uninit() { this._view.destruct(); } }; /** * Notifies a DownloadsView about updates in the state of various downloads. * * @param aView An instance of DownloadsView. */ function DownloadProgressListener(aView) { this._view = aView; } DownloadProgressListener.prototype = { _view: null, ////////////////////////////////////////////////////////////////////////////// //// nsIDownloadProgressListener onDownloadStateChange: function dPL_onDownloadStateChange(aState, aDownload) { let state = aDownload.state; switch (state) { case Ci.nsIDownloadManager.DOWNLOAD_QUEUED: case Ci.nsIDownloadManager.DOWNLOAD_BLOCKED_POLICY: this._view.addDownload(aDownload); break; case Ci.nsIDownloadManager.DOWNLOAD_FAILED: case Ci.nsIDownloadManager.DOWNLOAD_CANCELED: case Ci.nsIDownloadManager.DOWNLOAD_BLOCKED_PARENTAL: case Ci.nsIDownloadManager.DOWNLOAD_DIRTY: case Ci.nsIDownloadManager.DOWNLOAD_FINISHED: break; } this._view.updateDownload(aDownload); }, onProgressChange: function dPL_onProgressChange(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress, aDownload) { // TODO : Add more detailed progress information. this._view.updateDownload(aDownload); }, onStateChange: function(aWebProgress, aRequest, aState, aStatus, aDownload) { }, onSecurityChange: function(aWebProgress, aRequest, aState, aDownload) { }, ////////////////////////////////////////////////////////////////////////////// //// nsISupports QueryInterface: XPCOMUtils.generateQI([Ci.nsIDownloadProgressListener]) }; /** * Tracks download progress so that additional information can be displayed * about its download in alert popups. */ function AlertDownloadProgressListener() { } AlertDownloadProgressListener.prototype = { ////////////////////////////////////////////////////////////////////////////// //// nsIDownloadProgressListener onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress, aDownload) { let strings = Strings.browser; let availableSpace = -1; try { // diskSpaceAvailable is not implemented on all systems let availableSpace = aDownload.targetFile.diskSpaceAvailable; } catch(ex) { } let contentLength = aDownload.size; if (availableSpace > 0 && contentLength > 0 && contentLength > availableSpace) { Downloads.showAlert(aDownload.target.spec.replace("file:", "download:"), strings.GetStringFromName("alertDownloadsNoSpace"), strings.GetStringFromName("alertDownloadsSize")); Downloads.cancelDownload(aDownload.id); } }, onDownloadStateChange: function(aState, aDownload) { }, onStateChange: function(aWebProgress, aRequest, aState, aStatus, aDownload) { }, onSecurityChange: function(aWebProgress, aRequest, aState, aDownload) { }, ////////////////////////////////////////////////////////////////////////////// //// nsISupports QueryInterface: XPCOMUtils.generateQI([Ci.nsIDownloadProgressListener]) };