browser/components/downloads/src/DownloadsCommon.jsm

Wed, 31 Dec 2014 13:27:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 13:27:57 +0100
branch
TOR_BUG_3246
changeset 6
8bccb770b82d
permissions
-rw-r--r--

Ignore runtime configuration files generated during quality assurance.

michael@0 1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
michael@0 2 /* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
michael@0 3 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
michael@0 5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 6
michael@0 7 "use strict";
michael@0 8
michael@0 9 this.EXPORTED_SYMBOLS = [
michael@0 10 "DownloadsCommon",
michael@0 11 ];
michael@0 12
michael@0 13 /**
michael@0 14 * Handles the Downloads panel shared methods and data access.
michael@0 15 *
michael@0 16 * This file includes the following constructors and global objects:
michael@0 17 *
michael@0 18 * DownloadsCommon
michael@0 19 * This object is exposed directly to the consumers of this JavaScript module,
michael@0 20 * and provides shared methods for all the instances of the user interface.
michael@0 21 *
michael@0 22 * DownloadsData
michael@0 23 * Retrieves the list of past and completed downloads from the underlying
michael@0 24 * Download Manager data, and provides asynchronous notifications allowing
michael@0 25 * to build a consistent view of the available data.
michael@0 26 *
michael@0 27 * DownloadsDataItem
michael@0 28 * Represents a single item in the list of downloads. This object either wraps
michael@0 29 * an existing nsIDownload from the Download Manager, or provides the same
michael@0 30 * information read directly from the downloads database, with the possibility
michael@0 31 * of querying the nsIDownload lazily, for performance reasons.
michael@0 32 *
michael@0 33 * DownloadsIndicatorData
michael@0 34 * This object registers itself with DownloadsData as a view, and transforms the
michael@0 35 * notifications it receives into overall status data, that is then broadcast to
michael@0 36 * the registered download status indicators.
michael@0 37 */
michael@0 38
michael@0 39 ////////////////////////////////////////////////////////////////////////////////
michael@0 40 //// Globals
michael@0 41
michael@0 42 const Cc = Components.classes;
michael@0 43 const Ci = Components.interfaces;
michael@0 44 const Cu = Components.utils;
michael@0 45 const Cr = Components.results;
michael@0 46
michael@0 47 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
michael@0 48 Cu.import("resource://gre/modules/Services.jsm");
michael@0 49
michael@0 50 XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
michael@0 51 "resource://gre/modules/NetUtil.jsm");
michael@0 52 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
michael@0 53 "resource://gre/modules/PluralForm.jsm");
michael@0 54 XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
michael@0 55 "resource://gre/modules/Downloads.jsm");
michael@0 56 XPCOMUtils.defineLazyModuleGetter(this, "DownloadUIHelper",
michael@0 57 "resource://gre/modules/DownloadUIHelper.jsm");
michael@0 58 XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
michael@0 59 "resource://gre/modules/DownloadUtils.jsm");
michael@0 60 XPCOMUtils.defineLazyModuleGetter(this, "OS",
michael@0 61 "resource://gre/modules/osfile.jsm")
michael@0 62 XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
michael@0 63 "resource://gre/modules/PlacesUtils.jsm");
michael@0 64 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
michael@0 65 "resource://gre/modules/PrivateBrowsingUtils.jsm");
michael@0 66 XPCOMUtils.defineLazyModuleGetter(this, "RecentWindow",
michael@0 67 "resource:///modules/RecentWindow.jsm");
michael@0 68 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
michael@0 69 "resource://gre/modules/Promise.jsm");
michael@0 70 XPCOMUtils.defineLazyModuleGetter(this, "DownloadsLogger",
michael@0 71 "resource:///modules/DownloadsLogger.jsm");
michael@0 72
michael@0 73 const nsIDM = Ci.nsIDownloadManager;
michael@0 74
michael@0 75 const kDownloadsStringBundleUrl =
michael@0 76 "chrome://browser/locale/downloads/downloads.properties";
michael@0 77
michael@0 78 const kDownloadsStringsRequiringFormatting = {
michael@0 79 sizeWithUnits: true,
michael@0 80 shortTimeLeftSeconds: true,
michael@0 81 shortTimeLeftMinutes: true,
michael@0 82 shortTimeLeftHours: true,
michael@0 83 shortTimeLeftDays: true,
michael@0 84 statusSeparator: true,
michael@0 85 statusSeparatorBeforeNumber: true,
michael@0 86 fileExecutableSecurityWarning: true
michael@0 87 };
michael@0 88
michael@0 89 const kDownloadsStringsRequiringPluralForm = {
michael@0 90 otherDownloads2: true
michael@0 91 };
michael@0 92
michael@0 93 XPCOMUtils.defineLazyGetter(this, "DownloadsLocalFileCtor", function () {
michael@0 94 return Components.Constructor("@mozilla.org/file/local;1",
michael@0 95 "nsILocalFile", "initWithPath");
michael@0 96 });
michael@0 97
michael@0 98 const kPartialDownloadSuffix = ".part";
michael@0 99
michael@0 100 const kPrefBranch = Services.prefs.getBranch("browser.download.");
michael@0 101
michael@0 102 let PrefObserver = {
michael@0 103 QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
michael@0 104 Ci.nsISupportsWeakReference]),
michael@0 105 getPref: function PO_getPref(name) {
michael@0 106 try {
michael@0 107 switch (typeof this.prefs[name]) {
michael@0 108 case "boolean":
michael@0 109 return kPrefBranch.getBoolPref(name);
michael@0 110 }
michael@0 111 } catch (ex) { }
michael@0 112 return this.prefs[name];
michael@0 113 },
michael@0 114 observe: function PO_observe(aSubject, aTopic, aData) {
michael@0 115 if (this.prefs.hasOwnProperty(aData)) {
michael@0 116 return this[aData] = this.getPref(aData);
michael@0 117 }
michael@0 118 },
michael@0 119 register: function PO_register(prefs) {
michael@0 120 this.prefs = prefs;
michael@0 121 kPrefBranch.addObserver("", this, true);
michael@0 122 for (let key in prefs) {
michael@0 123 let name = key;
michael@0 124 XPCOMUtils.defineLazyGetter(this, name, function () {
michael@0 125 return PrefObserver.getPref(name);
michael@0 126 });
michael@0 127 }
michael@0 128 },
michael@0 129 };
michael@0 130
michael@0 131 PrefObserver.register({
michael@0 132 // prefName: defaultValue
michael@0 133 debug: false,
michael@0 134 animateNotifications: true
michael@0 135 });
michael@0 136
michael@0 137
michael@0 138 ////////////////////////////////////////////////////////////////////////////////
michael@0 139 //// DownloadsCommon
michael@0 140
michael@0 141 /**
michael@0 142 * This object is exposed directly to the consumers of this JavaScript module,
michael@0 143 * and provides shared methods for all the instances of the user interface.
michael@0 144 */
michael@0 145 this.DownloadsCommon = {
michael@0 146 log: function DC_log(...aMessageArgs) {
michael@0 147 delete this.log;
michael@0 148 this.log = function DC_log(...aMessageArgs) {
michael@0 149 if (!PrefObserver.debug) {
michael@0 150 return;
michael@0 151 }
michael@0 152 DownloadsLogger.log.apply(DownloadsLogger, aMessageArgs);
michael@0 153 }
michael@0 154 this.log.apply(this, aMessageArgs);
michael@0 155 },
michael@0 156
michael@0 157 error: function DC_error(...aMessageArgs) {
michael@0 158 delete this.error;
michael@0 159 this.error = function DC_error(...aMessageArgs) {
michael@0 160 if (!PrefObserver.debug) {
michael@0 161 return;
michael@0 162 }
michael@0 163 DownloadsLogger.reportError.apply(DownloadsLogger, aMessageArgs);
michael@0 164 }
michael@0 165 this.error.apply(this, aMessageArgs);
michael@0 166 },
michael@0 167 /**
michael@0 168 * Returns an object whose keys are the string names from the downloads string
michael@0 169 * bundle, and whose values are either the translated strings or functions
michael@0 170 * returning formatted strings.
michael@0 171 */
michael@0 172 get strings()
michael@0 173 {
michael@0 174 let strings = {};
michael@0 175 let sb = Services.strings.createBundle(kDownloadsStringBundleUrl);
michael@0 176 let enumerator = sb.getSimpleEnumeration();
michael@0 177 while (enumerator.hasMoreElements()) {
michael@0 178 let string = enumerator.getNext().QueryInterface(Ci.nsIPropertyElement);
michael@0 179 let stringName = string.key;
michael@0 180 if (stringName in kDownloadsStringsRequiringFormatting) {
michael@0 181 strings[stringName] = function () {
michael@0 182 // Convert "arguments" to a real array before calling into XPCOM.
michael@0 183 return sb.formatStringFromName(stringName,
michael@0 184 Array.slice(arguments, 0),
michael@0 185 arguments.length);
michael@0 186 };
michael@0 187 } else if (stringName in kDownloadsStringsRequiringPluralForm) {
michael@0 188 strings[stringName] = function (aCount) {
michael@0 189 // Convert "arguments" to a real array before calling into XPCOM.
michael@0 190 let formattedString = sb.formatStringFromName(stringName,
michael@0 191 Array.slice(arguments, 0),
michael@0 192 arguments.length);
michael@0 193 return PluralForm.get(aCount, formattedString);
michael@0 194 };
michael@0 195 } else {
michael@0 196 strings[stringName] = string.value;
michael@0 197 }
michael@0 198 }
michael@0 199 delete this.strings;
michael@0 200 return this.strings = strings;
michael@0 201 },
michael@0 202
michael@0 203 /**
michael@0 204 * Generates a very short string representing the given time left.
michael@0 205 *
michael@0 206 * @param aSeconds
michael@0 207 * Value to be formatted. It represents the number of seconds, it must
michael@0 208 * be positive but does not need to be an integer.
michael@0 209 *
michael@0 210 * @return Formatted string, for example "30s" or "2h". The returned value is
michael@0 211 * maximum three characters long, at least in English.
michael@0 212 */
michael@0 213 formatTimeLeft: function DC_formatTimeLeft(aSeconds)
michael@0 214 {
michael@0 215 // Decide what text to show for the time
michael@0 216 let seconds = Math.round(aSeconds);
michael@0 217 if (!seconds) {
michael@0 218 return "";
michael@0 219 } else if (seconds <= 30) {
michael@0 220 return DownloadsCommon.strings["shortTimeLeftSeconds"](seconds);
michael@0 221 }
michael@0 222 let minutes = Math.round(aSeconds / 60);
michael@0 223 if (minutes < 60) {
michael@0 224 return DownloadsCommon.strings["shortTimeLeftMinutes"](minutes);
michael@0 225 }
michael@0 226 let hours = Math.round(minutes / 60);
michael@0 227 if (hours < 48) { // two days
michael@0 228 return DownloadsCommon.strings["shortTimeLeftHours"](hours);
michael@0 229 }
michael@0 230 let days = Math.round(hours / 24);
michael@0 231 return DownloadsCommon.strings["shortTimeLeftDays"](Math.min(days, 99));
michael@0 232 },
michael@0 233
michael@0 234 /**
michael@0 235 * Indicates whether we should show visual notification on the indicator
michael@0 236 * when a download event is triggered.
michael@0 237 */
michael@0 238 get animateNotifications()
michael@0 239 {
michael@0 240 return PrefObserver.animateNotifications;
michael@0 241 },
michael@0 242
michael@0 243 /**
michael@0 244 * Get access to one of the DownloadsData or PrivateDownloadsData objects,
michael@0 245 * depending on the privacy status of the window in question.
michael@0 246 *
michael@0 247 * @param aWindow
michael@0 248 * The browser window which owns the download button.
michael@0 249 */
michael@0 250 getData: function DC_getData(aWindow) {
michael@0 251 if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
michael@0 252 return PrivateDownloadsData;
michael@0 253 } else {
michael@0 254 return DownloadsData;
michael@0 255 }
michael@0 256 },
michael@0 257
michael@0 258 /**
michael@0 259 * Initializes the Downloads back-end and starts receiving events for both the
michael@0 260 * private and non-private downloads data objects.
michael@0 261 */
michael@0 262 initializeAllDataLinks: function () {
michael@0 263 DownloadsData.initializeDataLink();
michael@0 264 PrivateDownloadsData.initializeDataLink();
michael@0 265 },
michael@0 266
michael@0 267 /**
michael@0 268 * Get access to one of the DownloadsIndicatorData or
michael@0 269 * PrivateDownloadsIndicatorData objects, depending on the privacy status of
michael@0 270 * the window in question.
michael@0 271 */
michael@0 272 getIndicatorData: function DC_getIndicatorData(aWindow) {
michael@0 273 if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
michael@0 274 return PrivateDownloadsIndicatorData;
michael@0 275 } else {
michael@0 276 return DownloadsIndicatorData;
michael@0 277 }
michael@0 278 },
michael@0 279
michael@0 280 /**
michael@0 281 * Returns a reference to the DownloadsSummaryData singleton - creating one
michael@0 282 * in the process if one hasn't been instantiated yet.
michael@0 283 *
michael@0 284 * @param aWindow
michael@0 285 * The browser window which owns the download button.
michael@0 286 * @param aNumToExclude
michael@0 287 * The number of items on the top of the downloads list to exclude
michael@0 288 * from the summary.
michael@0 289 */
michael@0 290 getSummary: function DC_getSummary(aWindow, aNumToExclude)
michael@0 291 {
michael@0 292 if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
michael@0 293 if (this._privateSummary) {
michael@0 294 return this._privateSummary;
michael@0 295 }
michael@0 296 return this._privateSummary = new DownloadsSummaryData(true, aNumToExclude);
michael@0 297 } else {
michael@0 298 if (this._summary) {
michael@0 299 return this._summary;
michael@0 300 }
michael@0 301 return this._summary = new DownloadsSummaryData(false, aNumToExclude);
michael@0 302 }
michael@0 303 },
michael@0 304 _summary: null,
michael@0 305 _privateSummary: null,
michael@0 306
michael@0 307 /**
michael@0 308 * Given an iterable collection of DownloadDataItems, generates and returns
michael@0 309 * statistics about that collection.
michael@0 310 *
michael@0 311 * @param aDataItems An iterable collection of DownloadDataItems.
michael@0 312 *
michael@0 313 * @return Object whose properties are the generated statistics. Currently,
michael@0 314 * we return the following properties:
michael@0 315 *
michael@0 316 * numActive : The total number of downloads.
michael@0 317 * numPaused : The total number of paused downloads.
michael@0 318 * numScanning : The total number of downloads being scanned.
michael@0 319 * numDownloading : The total number of downloads being downloaded.
michael@0 320 * totalSize : The total size of all downloads once completed.
michael@0 321 * totalTransferred: The total amount of transferred data for these
michael@0 322 * downloads.
michael@0 323 * slowestSpeed : The slowest download rate.
michael@0 324 * rawTimeLeft : The estimated time left for the downloads to
michael@0 325 * complete.
michael@0 326 * percentComplete : The percentage of bytes successfully downloaded.
michael@0 327 */
michael@0 328 summarizeDownloads: function DC_summarizeDownloads(aDataItems)
michael@0 329 {
michael@0 330 let summary = {
michael@0 331 numActive: 0,
michael@0 332 numPaused: 0,
michael@0 333 numScanning: 0,
michael@0 334 numDownloading: 0,
michael@0 335 totalSize: 0,
michael@0 336 totalTransferred: 0,
michael@0 337 // slowestSpeed is Infinity so that we can use Math.min to
michael@0 338 // find the slowest speed. We'll set this to 0 afterwards if
michael@0 339 // it's still at Infinity by the time we're done iterating all
michael@0 340 // dataItems.
michael@0 341 slowestSpeed: Infinity,
michael@0 342 rawTimeLeft: -1,
michael@0 343 percentComplete: -1
michael@0 344 }
michael@0 345
michael@0 346 for (let dataItem of aDataItems) {
michael@0 347 summary.numActive++;
michael@0 348 switch (dataItem.state) {
michael@0 349 case nsIDM.DOWNLOAD_PAUSED:
michael@0 350 summary.numPaused++;
michael@0 351 break;
michael@0 352 case nsIDM.DOWNLOAD_SCANNING:
michael@0 353 summary.numScanning++;
michael@0 354 break;
michael@0 355 case nsIDM.DOWNLOAD_DOWNLOADING:
michael@0 356 summary.numDownloading++;
michael@0 357 if (dataItem.maxBytes > 0 && dataItem.speed > 0) {
michael@0 358 let sizeLeft = dataItem.maxBytes - dataItem.currBytes;
michael@0 359 summary.rawTimeLeft = Math.max(summary.rawTimeLeft,
michael@0 360 sizeLeft / dataItem.speed);
michael@0 361 summary.slowestSpeed = Math.min(summary.slowestSpeed,
michael@0 362 dataItem.speed);
michael@0 363 }
michael@0 364 break;
michael@0 365 }
michael@0 366 // Only add to total values if we actually know the download size.
michael@0 367 if (dataItem.maxBytes > 0 &&
michael@0 368 dataItem.state != nsIDM.DOWNLOAD_CANCELED &&
michael@0 369 dataItem.state != nsIDM.DOWNLOAD_FAILED) {
michael@0 370 summary.totalSize += dataItem.maxBytes;
michael@0 371 summary.totalTransferred += dataItem.currBytes;
michael@0 372 }
michael@0 373 }
michael@0 374
michael@0 375 if (summary.numActive != 0 && summary.totalSize != 0 &&
michael@0 376 summary.numActive != summary.numScanning) {
michael@0 377 summary.percentComplete = (summary.totalTransferred /
michael@0 378 summary.totalSize) * 100;
michael@0 379 }
michael@0 380
michael@0 381 if (summary.slowestSpeed == Infinity) {
michael@0 382 summary.slowestSpeed = 0;
michael@0 383 }
michael@0 384
michael@0 385 return summary;
michael@0 386 },
michael@0 387
michael@0 388 /**
michael@0 389 * If necessary, smooths the estimated number of seconds remaining for one
michael@0 390 * or more downloads to complete.
michael@0 391 *
michael@0 392 * @param aSeconds
michael@0 393 * Current raw estimate on number of seconds left for one or more
michael@0 394 * downloads. This is a floating point value to help get sub-second
michael@0 395 * accuracy for current and future estimates.
michael@0 396 */
michael@0 397 smoothSeconds: function DC_smoothSeconds(aSeconds, aLastSeconds)
michael@0 398 {
michael@0 399 // We apply an algorithm similar to the DownloadUtils.getTimeLeft function,
michael@0 400 // though tailored to a single time estimation for all downloads. We never
michael@0 401 // apply sommothing if the new value is less than half the previous value.
michael@0 402 let shouldApplySmoothing = aLastSeconds >= 0 &&
michael@0 403 aSeconds > aLastSeconds / 2;
michael@0 404 if (shouldApplySmoothing) {
michael@0 405 // Apply hysteresis to favor downward over upward swings. Trust only 30%
michael@0 406 // of the new value if lower, and 10% if higher (exponential smoothing).
michael@0 407 let (diff = aSeconds - aLastSeconds) {
michael@0 408 aSeconds = aLastSeconds + (diff < 0 ? .3 : .1) * diff;
michael@0 409 }
michael@0 410
michael@0 411 // If the new time is similar, reuse something close to the last time
michael@0 412 // left, but subtract a little to provide forward progress.
michael@0 413 let diff = aSeconds - aLastSeconds;
michael@0 414 let diffPercent = diff / aLastSeconds * 100;
michael@0 415 if (Math.abs(diff) < 5 || Math.abs(diffPercent) < 5) {
michael@0 416 aSeconds = aLastSeconds - (diff < 0 ? .4 : .2);
michael@0 417 }
michael@0 418 }
michael@0 419
michael@0 420 // In the last few seconds of downloading, we are always subtracting and
michael@0 421 // never adding to the time left. Ensure that we never fall below one
michael@0 422 // second left until all downloads are actually finished.
michael@0 423 return aLastSeconds = Math.max(aSeconds, 1);
michael@0 424 },
michael@0 425
michael@0 426 /**
michael@0 427 * Opens a downloaded file.
michael@0 428 * If you've a dataItem, you should call dataItem.openLocalFile.
michael@0 429 * @param aFile
michael@0 430 * the downloaded file to be opened.
michael@0 431 * @param aMimeInfo
michael@0 432 * the mime type info object. May be null.
michael@0 433 * @param aOwnerWindow
michael@0 434 * the window with which this action is associated.
michael@0 435 */
michael@0 436 openDownloadedFile: function DC_openDownloadedFile(aFile, aMimeInfo, aOwnerWindow) {
michael@0 437 if (!(aFile instanceof Ci.nsIFile))
michael@0 438 throw new Error("aFile must be a nsIFile object");
michael@0 439 if (aMimeInfo && !(aMimeInfo instanceof Ci.nsIMIMEInfo))
michael@0 440 throw new Error("Invalid value passed for aMimeInfo");
michael@0 441 if (!(aOwnerWindow instanceof Ci.nsIDOMWindow))
michael@0 442 throw new Error("aOwnerWindow must be a dom-window object");
michael@0 443
michael@0 444 let promiseShouldLaunch;
michael@0 445 if (aFile.isExecutable()) {
michael@0 446 // We get a prompter for the provided window here, even though anchoring
michael@0 447 // to the most recently active window should work as well.
michael@0 448 promiseShouldLaunch =
michael@0 449 DownloadUIHelper.getPrompter(aOwnerWindow)
michael@0 450 .confirmLaunchExecutable(aFile.path);
michael@0 451 } else {
michael@0 452 promiseShouldLaunch = Promise.resolve(true);
michael@0 453 }
michael@0 454
michael@0 455 promiseShouldLaunch.then(shouldLaunch => {
michael@0 456 if (!shouldLaunch) {
michael@0 457 return;
michael@0 458 }
michael@0 459
michael@0 460 // Actually open the file.
michael@0 461 try {
michael@0 462 if (aMimeInfo && aMimeInfo.preferredAction == aMimeInfo.useHelperApp) {
michael@0 463 aMimeInfo.launchWithFile(aFile);
michael@0 464 return;
michael@0 465 }
michael@0 466 }
michael@0 467 catch(ex) { }
michael@0 468
michael@0 469 // If either we don't have the mime info, or the preferred action failed,
michael@0 470 // attempt to launch the file directly.
michael@0 471 try {
michael@0 472 aFile.launch();
michael@0 473 }
michael@0 474 catch(ex) {
michael@0 475 // If launch fails, try sending it through the system's external "file:"
michael@0 476 // URL handler.
michael@0 477 Cc["@mozilla.org/uriloader/external-protocol-service;1"]
michael@0 478 .getService(Ci.nsIExternalProtocolService)
michael@0 479 .loadUrl(NetUtil.newURI(aFile));
michael@0 480 }
michael@0 481 }).then(null, Cu.reportError);
michael@0 482 },
michael@0 483
michael@0 484 /**
michael@0 485 * Show a donwloaded file in the system file manager.
michael@0 486 * If you have a dataItem, use dataItem.showLocalFile.
michael@0 487 *
michael@0 488 * @param aFile
michael@0 489 * a downloaded file.
michael@0 490 */
michael@0 491 showDownloadedFile: function DC_showDownloadedFile(aFile) {
michael@0 492 if (!(aFile instanceof Ci.nsIFile))
michael@0 493 throw new Error("aFile must be a nsIFile object");
michael@0 494 try {
michael@0 495 // Show the directory containing the file and select the file.
michael@0 496 aFile.reveal();
michael@0 497 } catch (ex) {
michael@0 498 // If reveal fails for some reason (e.g., it's not implemented on unix
michael@0 499 // or the file doesn't exist), try using the parent if we have it.
michael@0 500 let parent = aFile.parent;
michael@0 501 if (parent) {
michael@0 502 try {
michael@0 503 // Open the parent directory to show where the file should be.
michael@0 504 parent.launch();
michael@0 505 } catch (ex) {
michael@0 506 // If launch also fails (probably because it's not implemented), let
michael@0 507 // the OS handler try to open the parent.
michael@0 508 Cc["@mozilla.org/uriloader/external-protocol-service;1"]
michael@0 509 .getService(Ci.nsIExternalProtocolService)
michael@0 510 .loadUrl(NetUtil.newURI(parent));
michael@0 511 }
michael@0 512 }
michael@0 513 }
michael@0 514 }
michael@0 515 };
michael@0 516
michael@0 517 /**
michael@0 518 * Returns true if we are executing on Windows Vista or a later version.
michael@0 519 */
michael@0 520 XPCOMUtils.defineLazyGetter(DownloadsCommon, "isWinVistaOrHigher", function () {
michael@0 521 let os = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
michael@0 522 if (os != "WINNT") {
michael@0 523 return false;
michael@0 524 }
michael@0 525 let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
michael@0 526 return parseFloat(sysInfo.getProperty("version")) >= 6;
michael@0 527 });
michael@0 528
michael@0 529 ////////////////////////////////////////////////////////////////////////////////
michael@0 530 //// DownloadsData
michael@0 531
michael@0 532 /**
michael@0 533 * Retrieves the list of past and completed downloads from the underlying
michael@0 534 * Download Manager data, and provides asynchronous notifications allowing to
michael@0 535 * build a consistent view of the available data.
michael@0 536 *
michael@0 537 * This object responds to real-time changes in the underlying Download Manager
michael@0 538 * data. For example, the deletion of one or more downloads is notified through
michael@0 539 * the nsIObserver interface, while any state or progress change is notified
michael@0 540 * through the nsIDownloadProgressListener interface.
michael@0 541 *
michael@0 542 * Note that using this object does not automatically start the Download Manager
michael@0 543 * service. Consumers will see an empty list of downloads until the service is
michael@0 544 * actually started. This is useful to display a neutral progress indicator in
michael@0 545 * the main browser window until the autostart timeout elapses.
michael@0 546 *
michael@0 547 * Note that DownloadsData and PrivateDownloadsData are two equivalent singleton
michael@0 548 * objects, one accessing non-private downloads, and the other accessing private
michael@0 549 * ones.
michael@0 550 */
michael@0 551 function DownloadsDataCtor(aPrivate) {
michael@0 552 this._isPrivate = aPrivate;
michael@0 553
michael@0 554 // This Object contains all the available DownloadsDataItem objects, indexed by
michael@0 555 // their globally unique identifier. The identifiers of downloads that have
michael@0 556 // been removed from the Download Manager data are still present, however the
michael@0 557 // associated objects are replaced with the value "null". This is required to
michael@0 558 // prevent race conditions when populating the list asynchronously.
michael@0 559 this.dataItems = {};
michael@0 560
michael@0 561 // Array of view objects that should be notified when the available download
michael@0 562 // data changes.
michael@0 563 this._views = [];
michael@0 564
michael@0 565 // Maps Download objects to DownloadDataItem objects.
michael@0 566 this._downloadToDataItemMap = new Map();
michael@0 567 }
michael@0 568
michael@0 569 DownloadsDataCtor.prototype = {
michael@0 570 /**
michael@0 571 * Starts receiving events for current downloads.
michael@0 572 */
michael@0 573 initializeDataLink: function ()
michael@0 574 {
michael@0 575 if (!this._dataLinkInitialized) {
michael@0 576 let promiseList = Downloads.getList(this._isPrivate ? Downloads.PRIVATE
michael@0 577 : Downloads.PUBLIC);
michael@0 578 promiseList.then(list => list.addView(this)).then(null, Cu.reportError);
michael@0 579 this._dataLinkInitialized = true;
michael@0 580 }
michael@0 581 },
michael@0 582 _dataLinkInitialized: false,
michael@0 583
michael@0 584 /**
michael@0 585 * True if there are finished downloads that can be removed from the list.
michael@0 586 */
michael@0 587 get canRemoveFinished()
michael@0 588 {
michael@0 589 for (let [, dataItem] of Iterator(this.dataItems)) {
michael@0 590 if (dataItem && !dataItem.inProgress) {
michael@0 591 return true;
michael@0 592 }
michael@0 593 }
michael@0 594 return false;
michael@0 595 },
michael@0 596
michael@0 597 /**
michael@0 598 * Asks the back-end to remove finished downloads from the list.
michael@0 599 */
michael@0 600 removeFinished: function DD_removeFinished()
michael@0 601 {
michael@0 602 let promiseList = Downloads.getList(this._isPrivate ? Downloads.PRIVATE
michael@0 603 : Downloads.PUBLIC);
michael@0 604 promiseList.then(list => list.removeFinished())
michael@0 605 .then(null, Cu.reportError);
michael@0 606 },
michael@0 607
michael@0 608 //////////////////////////////////////////////////////////////////////////////
michael@0 609 //// Integration with the asynchronous Downloads back-end
michael@0 610
michael@0 611 onDownloadAdded: function (aDownload)
michael@0 612 {
michael@0 613 let dataItem = new DownloadsDataItem(aDownload);
michael@0 614 this._downloadToDataItemMap.set(aDownload, dataItem);
michael@0 615 this.dataItems[dataItem.downloadGuid] = dataItem;
michael@0 616
michael@0 617 for (let view of this._views) {
michael@0 618 view.onDataItemAdded(dataItem, true);
michael@0 619 }
michael@0 620
michael@0 621 this._updateDataItemState(dataItem);
michael@0 622 },
michael@0 623
michael@0 624 onDownloadChanged: function (aDownload)
michael@0 625 {
michael@0 626 let dataItem = this._downloadToDataItemMap.get(aDownload);
michael@0 627 if (!dataItem) {
michael@0 628 Cu.reportError("Download doesn't exist.");
michael@0 629 return;
michael@0 630 }
michael@0 631
michael@0 632 this._updateDataItemState(dataItem);
michael@0 633 },
michael@0 634
michael@0 635 onDownloadRemoved: function (aDownload)
michael@0 636 {
michael@0 637 let dataItem = this._downloadToDataItemMap.get(aDownload);
michael@0 638 if (!dataItem) {
michael@0 639 Cu.reportError("Download doesn't exist.");
michael@0 640 return;
michael@0 641 }
michael@0 642
michael@0 643 this._downloadToDataItemMap.delete(aDownload);
michael@0 644 this.dataItems[dataItem.downloadGuid] = null;
michael@0 645 for (let view of this._views) {
michael@0 646 view.onDataItemRemoved(dataItem);
michael@0 647 }
michael@0 648 },
michael@0 649
michael@0 650 /**
michael@0 651 * Updates the given data item and sends related notifications.
michael@0 652 */
michael@0 653 _updateDataItemState: function (aDataItem)
michael@0 654 {
michael@0 655 let oldState = aDataItem.state;
michael@0 656 let wasInProgress = aDataItem.inProgress;
michael@0 657 let wasDone = aDataItem.done;
michael@0 658
michael@0 659 aDataItem.updateFromDownload();
michael@0 660
michael@0 661 if (wasInProgress && !aDataItem.inProgress) {
michael@0 662 aDataItem.endTime = Date.now();
michael@0 663 }
michael@0 664
michael@0 665 if (oldState != aDataItem.state) {
michael@0 666 for (let view of this._views) {
michael@0 667 try {
michael@0 668 view.getViewItem(aDataItem).onStateChange(oldState);
michael@0 669 } catch (ex) {
michael@0 670 Cu.reportError(ex);
michael@0 671 }
michael@0 672 }
michael@0 673
michael@0 674 // This state transition code should actually be located in a Downloads
michael@0 675 // API module (bug 941009). Moreover, the fact that state is stored as
michael@0 676 // annotations should be ideally hidden behind methods of
michael@0 677 // nsIDownloadHistory (bug 830415).
michael@0 678 if (!this._isPrivate && !aDataItem.inProgress) {
michael@0 679 try {
michael@0 680 let downloadMetaData = { state: aDataItem.state,
michael@0 681 endTime: aDataItem.endTime };
michael@0 682 if (aDataItem.done) {
michael@0 683 downloadMetaData.fileSize = aDataItem.maxBytes;
michael@0 684 }
michael@0 685
michael@0 686 PlacesUtils.annotations.setPageAnnotation(
michael@0 687 NetUtil.newURI(aDataItem.uri), "downloads/metaData",
michael@0 688 JSON.stringify(downloadMetaData), 0,
michael@0 689 PlacesUtils.annotations.EXPIRE_WITH_HISTORY);
michael@0 690 } catch (ex) {
michael@0 691 Cu.reportError(ex);
michael@0 692 }
michael@0 693 }
michael@0 694 }
michael@0 695
michael@0 696 if (!aDataItem.newDownloadNotified) {
michael@0 697 aDataItem.newDownloadNotified = true;
michael@0 698 this._notifyDownloadEvent("start");
michael@0 699 }
michael@0 700
michael@0 701 if (!wasDone && aDataItem.done) {
michael@0 702 this._notifyDownloadEvent("finish");
michael@0 703 }
michael@0 704
michael@0 705 for (let view of this._views) {
michael@0 706 view.getViewItem(aDataItem).onProgressChange();
michael@0 707 }
michael@0 708 },
michael@0 709
michael@0 710 //////////////////////////////////////////////////////////////////////////////
michael@0 711 //// Registration of views
michael@0 712
michael@0 713 /**
michael@0 714 * Adds an object to be notified when the available download data changes.
michael@0 715 * The specified object is initialized with the currently available downloads.
michael@0 716 *
michael@0 717 * @param aView
michael@0 718 * DownloadsView object to be added. This reference must be passed to
michael@0 719 * removeView before termination.
michael@0 720 */
michael@0 721 addView: function DD_addView(aView)
michael@0 722 {
michael@0 723 this._views.push(aView);
michael@0 724 this._updateView(aView);
michael@0 725 },
michael@0 726
michael@0 727 /**
michael@0 728 * Removes an object previously added using addView.
michael@0 729 *
michael@0 730 * @param aView
michael@0 731 * DownloadsView object to be removed.
michael@0 732 */
michael@0 733 removeView: function DD_removeView(aView)
michael@0 734 {
michael@0 735 let index = this._views.indexOf(aView);
michael@0 736 if (index != -1) {
michael@0 737 this._views.splice(index, 1);
michael@0 738 }
michael@0 739 },
michael@0 740
michael@0 741 /**
michael@0 742 * Ensures that the currently loaded data is added to the specified view.
michael@0 743 *
michael@0 744 * @param aView
michael@0 745 * DownloadsView object to be initialized.
michael@0 746 */
michael@0 747 _updateView: function DD_updateView(aView)
michael@0 748 {
michael@0 749 // Indicate to the view that a batch loading operation is in progress.
michael@0 750 aView.onDataLoadStarting();
michael@0 751
michael@0 752 // Sort backwards by start time, ensuring that the most recent
michael@0 753 // downloads are added first regardless of their state.
michael@0 754 let loadedItemsArray = [dataItem
michael@0 755 for each (dataItem in this.dataItems)
michael@0 756 if (dataItem)];
michael@0 757 loadedItemsArray.sort(function(a, b) b.startTime - a.startTime);
michael@0 758 loadedItemsArray.forEach(
michael@0 759 function (dataItem) aView.onDataItemAdded(dataItem, false)
michael@0 760 );
michael@0 761
michael@0 762 // Notify the view that all data is available.
michael@0 763 aView.onDataLoadCompleted();
michael@0 764 },
michael@0 765
michael@0 766 //////////////////////////////////////////////////////////////////////////////
michael@0 767 //// Notifications sent to the most recent browser window only
michael@0 768
michael@0 769 /**
michael@0 770 * Set to true after the first download causes the downloads panel to be
michael@0 771 * displayed.
michael@0 772 */
michael@0 773 get panelHasShownBefore() {
michael@0 774 try {
michael@0 775 return Services.prefs.getBoolPref("browser.download.panel.shown");
michael@0 776 } catch (ex) { }
michael@0 777 return false;
michael@0 778 },
michael@0 779
michael@0 780 set panelHasShownBefore(aValue) {
michael@0 781 Services.prefs.setBoolPref("browser.download.panel.shown", aValue);
michael@0 782 return aValue;
michael@0 783 },
michael@0 784
michael@0 785 /**
michael@0 786 * Displays a new or finished download notification in the most recent browser
michael@0 787 * window, if one is currently available with the required privacy type.
michael@0 788 *
michael@0 789 * @param aType
michael@0 790 * Set to "start" for new downloads, "finish" for completed downloads.
michael@0 791 */
michael@0 792 _notifyDownloadEvent: function DD_notifyDownloadEvent(aType)
michael@0 793 {
michael@0 794 DownloadsCommon.log("Attempting to notify that a new download has started or finished.");
michael@0 795
michael@0 796 // Show the panel in the most recent browser window, if present.
michael@0 797 let browserWin = RecentWindow.getMostRecentBrowserWindow({ private: this._isPrivate });
michael@0 798 if (!browserWin) {
michael@0 799 return;
michael@0 800 }
michael@0 801
michael@0 802 if (this.panelHasShownBefore) {
michael@0 803 // For new downloads after the first one, don't show the panel
michael@0 804 // automatically, but provide a visible notification in the topmost
michael@0 805 // browser window, if the status indicator is already visible.
michael@0 806 DownloadsCommon.log("Showing new download notification.");
michael@0 807 browserWin.DownloadsIndicatorView.showEventNotification(aType);
michael@0 808 return;
michael@0 809 }
michael@0 810 this.panelHasShownBefore = true;
michael@0 811 browserWin.DownloadsPanel.showPanel();
michael@0 812 }
michael@0 813 };
michael@0 814
michael@0 815 XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsData", function() {
michael@0 816 return new DownloadsDataCtor(true);
michael@0 817 });
michael@0 818
michael@0 819 XPCOMUtils.defineLazyGetter(this, "DownloadsData", function() {
michael@0 820 return new DownloadsDataCtor(false);
michael@0 821 });
michael@0 822
michael@0 823 ////////////////////////////////////////////////////////////////////////////////
michael@0 824 //// DownloadsDataItem
michael@0 825
michael@0 826 /**
michael@0 827 * Represents a single item in the list of downloads.
michael@0 828 *
michael@0 829 * The endTime property is initialized to the current date and time.
michael@0 830 *
michael@0 831 * @param aDownload
michael@0 832 * The Download object with the current state.
michael@0 833 */
michael@0 834 function DownloadsDataItem(aDownload)
michael@0 835 {
michael@0 836 this._download = aDownload;
michael@0 837
michael@0 838 this.downloadGuid = "id:" + this._autoIncrementId;
michael@0 839 this.file = aDownload.target.path;
michael@0 840 this.target = OS.Path.basename(aDownload.target.path);
michael@0 841 this.uri = aDownload.source.url;
michael@0 842 this.endTime = Date.now();
michael@0 843
michael@0 844 this.updateFromDownload();
michael@0 845 }
michael@0 846
michael@0 847 DownloadsDataItem.prototype = {
michael@0 848 /**
michael@0 849 * The JavaScript API does not need identifiers for Download objects, so they
michael@0 850 * are generated sequentially for the corresponding DownloadDataItem.
michael@0 851 */
michael@0 852 get _autoIncrementId() ++DownloadsDataItem.prototype.__lastId,
michael@0 853 __lastId: 0,
michael@0 854
michael@0 855 /**
michael@0 856 * Updates this object from the underlying Download object.
michael@0 857 */
michael@0 858 updateFromDownload: function ()
michael@0 859 {
michael@0 860 // Collapse state using the correct priority.
michael@0 861 if (this._download.succeeded) {
michael@0 862 this.state = nsIDM.DOWNLOAD_FINISHED;
michael@0 863 } else if (this._download.error &&
michael@0 864 this._download.error.becauseBlockedByParentalControls) {
michael@0 865 this.state = nsIDM.DOWNLOAD_BLOCKED_PARENTAL;
michael@0 866 } else if (this._download.error &&
michael@0 867 this._download.error.becauseBlockedByReputationCheck) {
michael@0 868 this.state = nsIDM.DOWNLOAD_DIRTY;
michael@0 869 } else if (this._download.error) {
michael@0 870 this.state = nsIDM.DOWNLOAD_FAILED;
michael@0 871 } else if (this._download.canceled && this._download.hasPartialData) {
michael@0 872 this.state = nsIDM.DOWNLOAD_PAUSED;
michael@0 873 } else if (this._download.canceled) {
michael@0 874 this.state = nsIDM.DOWNLOAD_CANCELED;
michael@0 875 } else if (this._download.stopped) {
michael@0 876 this.state = nsIDM.DOWNLOAD_NOTSTARTED;
michael@0 877 } else {
michael@0 878 this.state = nsIDM.DOWNLOAD_DOWNLOADING;
michael@0 879 }
michael@0 880
michael@0 881 this.referrer = this._download.source.referrer;
michael@0 882 this.startTime = this._download.startTime;
michael@0 883 this.currBytes = this._download.currentBytes;
michael@0 884 this.resumable = this._download.hasPartialData;
michael@0 885 this.speed = this._download.speed;
michael@0 886
michael@0 887 if (this._download.succeeded) {
michael@0 888 // If the download succeeded, show the final size if available, otherwise
michael@0 889 // use the last known number of bytes transferred. The final size on disk
michael@0 890 // will be available when bug 941063 is resolved.
michael@0 891 this.maxBytes = this._download.hasProgress ?
michael@0 892 this._download.totalBytes :
michael@0 893 this._download.currentBytes;
michael@0 894 this.percentComplete = 100;
michael@0 895 } else if (this._download.hasProgress) {
michael@0 896 // If the final size and progress are known, use them.
michael@0 897 this.maxBytes = this._download.totalBytes;
michael@0 898 this.percentComplete = this._download.progress;
michael@0 899 } else {
michael@0 900 // The download final size and progress percentage is unknown.
michael@0 901 this.maxBytes = -1;
michael@0 902 this.percentComplete = -1;
michael@0 903 }
michael@0 904 },
michael@0 905
michael@0 906 /**
michael@0 907 * Indicates whether the download is proceeding normally, and not finished
michael@0 908 * yet. This includes paused downloads. When this property is true, the
michael@0 909 * "progress" property represents the current progress of the download.
michael@0 910 */
michael@0 911 get inProgress()
michael@0 912 {
michael@0 913 return [
michael@0 914 nsIDM.DOWNLOAD_NOTSTARTED,
michael@0 915 nsIDM.DOWNLOAD_QUEUED,
michael@0 916 nsIDM.DOWNLOAD_DOWNLOADING,
michael@0 917 nsIDM.DOWNLOAD_PAUSED,
michael@0 918 nsIDM.DOWNLOAD_SCANNING,
michael@0 919 ].indexOf(this.state) != -1;
michael@0 920 },
michael@0 921
michael@0 922 /**
michael@0 923 * This is true during the initial phases of a download, before the actual
michael@0 924 * download of data bytes starts.
michael@0 925 */
michael@0 926 get starting()
michael@0 927 {
michael@0 928 return this.state == nsIDM.DOWNLOAD_NOTSTARTED ||
michael@0 929 this.state == nsIDM.DOWNLOAD_QUEUED;
michael@0 930 },
michael@0 931
michael@0 932 /**
michael@0 933 * Indicates whether the download is paused.
michael@0 934 */
michael@0 935 get paused()
michael@0 936 {
michael@0 937 return this.state == nsIDM.DOWNLOAD_PAUSED;
michael@0 938 },
michael@0 939
michael@0 940 /**
michael@0 941 * Indicates whether the download is in a final state, either because it
michael@0 942 * completed successfully or because it was blocked.
michael@0 943 */
michael@0 944 get done()
michael@0 945 {
michael@0 946 return [
michael@0 947 nsIDM.DOWNLOAD_FINISHED,
michael@0 948 nsIDM.DOWNLOAD_BLOCKED_PARENTAL,
michael@0 949 nsIDM.DOWNLOAD_BLOCKED_POLICY,
michael@0 950 nsIDM.DOWNLOAD_DIRTY,
michael@0 951 ].indexOf(this.state) != -1;
michael@0 952 },
michael@0 953
michael@0 954 /**
michael@0 955 * Indicates whether the download is finished and can be opened.
michael@0 956 */
michael@0 957 get openable()
michael@0 958 {
michael@0 959 return this.state == nsIDM.DOWNLOAD_FINISHED;
michael@0 960 },
michael@0 961
michael@0 962 /**
michael@0 963 * Indicates whether the download stopped because of an error, and can be
michael@0 964 * resumed manually.
michael@0 965 */
michael@0 966 get canRetry()
michael@0 967 {
michael@0 968 return this.state == nsIDM.DOWNLOAD_CANCELED ||
michael@0 969 this.state == nsIDM.DOWNLOAD_FAILED;
michael@0 970 },
michael@0 971
michael@0 972 /**
michael@0 973 * Returns the nsILocalFile for the download target.
michael@0 974 *
michael@0 975 * @throws if the native path is not valid. This can happen if the same
michael@0 976 * profile is used on different platforms, for example if a native
michael@0 977 * Windows path is stored and then the item is accessed on a Mac.
michael@0 978 */
michael@0 979 get localFile()
michael@0 980 {
michael@0 981 return this._getFile(this.file);
michael@0 982 },
michael@0 983
michael@0 984 /**
michael@0 985 * Returns the nsILocalFile for the partially downloaded target.
michael@0 986 *
michael@0 987 * @throws if the native path is not valid. This can happen if the same
michael@0 988 * profile is used on different platforms, for example if a native
michael@0 989 * Windows path is stored and then the item is accessed on a Mac.
michael@0 990 */
michael@0 991 get partFile()
michael@0 992 {
michael@0 993 return this._getFile(this.file + kPartialDownloadSuffix);
michael@0 994 },
michael@0 995
michael@0 996 /**
michael@0 997 * Returns an nsILocalFile for aFilename. aFilename might be a file URL or
michael@0 998 * a native path.
michael@0 999 *
michael@0 1000 * @param aFilename the filename of the file to retrieve.
michael@0 1001 * @return an nsILocalFile for the file.
michael@0 1002 * @throws if the native path is not valid. This can happen if the same
michael@0 1003 * profile is used on different platforms, for example if a native
michael@0 1004 * Windows path is stored and then the item is accessed on a Mac.
michael@0 1005 * @note This function makes no guarantees about the file's existence -
michael@0 1006 * callers should check that the returned file exists.
michael@0 1007 */
michael@0 1008 _getFile: function DDI__getFile(aFilename)
michael@0 1009 {
michael@0 1010 // The download database may contain targets stored as file URLs or native
michael@0 1011 // paths. This can still be true for previously stored items, even if new
michael@0 1012 // items are stored using their file URL. See also bug 239948 comment 12.
michael@0 1013 if (aFilename.startsWith("file:")) {
michael@0 1014 // Assume the file URL we obtained from the downloads database or from the
michael@0 1015 // "spec" property of the target has the UTF-8 charset.
michael@0 1016 let fileUrl = NetUtil.newURI(aFilename).QueryInterface(Ci.nsIFileURL);
michael@0 1017 return fileUrl.file.clone().QueryInterface(Ci.nsILocalFile);
michael@0 1018 } else {
michael@0 1019 // The downloads database contains a native path. Try to create a local
michael@0 1020 // file, though this may throw an exception if the path is invalid.
michael@0 1021 return new DownloadsLocalFileCtor(aFilename);
michael@0 1022 }
michael@0 1023 },
michael@0 1024
michael@0 1025 /**
michael@0 1026 * Open the target file for this download.
michael@0 1027 */
michael@0 1028 openLocalFile: function () {
michael@0 1029 this._download.launch().then(null, Cu.reportError);
michael@0 1030 },
michael@0 1031
michael@0 1032 /**
michael@0 1033 * Show the downloaded file in the system file manager.
michael@0 1034 */
michael@0 1035 showLocalFile: function DDI_showLocalFile() {
michael@0 1036 DownloadsCommon.showDownloadedFile(this.localFile);
michael@0 1037 },
michael@0 1038
michael@0 1039 /**
michael@0 1040 * Resumes the download if paused, pauses it if active.
michael@0 1041 * @throws if the download is not resumable or if has already done.
michael@0 1042 */
michael@0 1043 togglePauseResume: function DDI_togglePauseResume() {
michael@0 1044 if (this._download.stopped) {
michael@0 1045 this._download.start();
michael@0 1046 } else {
michael@0 1047 this._download.cancel();
michael@0 1048 }
michael@0 1049 },
michael@0 1050
michael@0 1051 /**
michael@0 1052 * Attempts to retry the download.
michael@0 1053 * @throws if we cannot.
michael@0 1054 */
michael@0 1055 retry: function DDI_retry() {
michael@0 1056 this._download.start();
michael@0 1057 },
michael@0 1058
michael@0 1059 /**
michael@0 1060 * Cancels the download.
michael@0 1061 */
michael@0 1062 cancel: function() {
michael@0 1063 this._download.cancel();
michael@0 1064 this._download.removePartialData().then(null, Cu.reportError);
michael@0 1065 },
michael@0 1066
michael@0 1067 /**
michael@0 1068 * Remove the download.
michael@0 1069 */
michael@0 1070 remove: function DDI_remove() {
michael@0 1071 Downloads.getList(Downloads.ALL)
michael@0 1072 .then(list => list.remove(this._download))
michael@0 1073 .then(() => this._download.finalize(true))
michael@0 1074 .then(null, Cu.reportError);
michael@0 1075 }
michael@0 1076 };
michael@0 1077
michael@0 1078 ////////////////////////////////////////////////////////////////////////////////
michael@0 1079 //// DownloadsViewPrototype
michael@0 1080
michael@0 1081 /**
michael@0 1082 * A prototype for an object that registers itself with DownloadsData as soon
michael@0 1083 * as a view is registered with it.
michael@0 1084 */
michael@0 1085 const DownloadsViewPrototype = {
michael@0 1086 //////////////////////////////////////////////////////////////////////////////
michael@0 1087 //// Registration of views
michael@0 1088
michael@0 1089 /**
michael@0 1090 * Array of view objects that should be notified when the available status
michael@0 1091 * data changes.
michael@0 1092 *
michael@0 1093 * SUBCLASSES MUST OVERRIDE THIS PROPERTY.
michael@0 1094 */
michael@0 1095 _views: null,
michael@0 1096
michael@0 1097 /**
michael@0 1098 * Determines whether this view object is over the private or non-private
michael@0 1099 * downloads.
michael@0 1100 *
michael@0 1101 * SUBCLASSES MUST OVERRIDE THIS PROPERTY.
michael@0 1102 */
michael@0 1103 _isPrivate: false,
michael@0 1104
michael@0 1105 /**
michael@0 1106 * Adds an object to be notified when the available status data changes.
michael@0 1107 * The specified object is initialized with the currently available status.
michael@0 1108 *
michael@0 1109 * @param aView
michael@0 1110 * View object to be added. This reference must be
michael@0 1111 * passed to removeView before termination.
michael@0 1112 */
michael@0 1113 addView: function DVP_addView(aView)
michael@0 1114 {
michael@0 1115 // Start receiving events when the first of our views is registered.
michael@0 1116 if (this._views.length == 0) {
michael@0 1117 if (this._isPrivate) {
michael@0 1118 PrivateDownloadsData.addView(this);
michael@0 1119 } else {
michael@0 1120 DownloadsData.addView(this);
michael@0 1121 }
michael@0 1122 }
michael@0 1123
michael@0 1124 this._views.push(aView);
michael@0 1125 this.refreshView(aView);
michael@0 1126 },
michael@0 1127
michael@0 1128 /**
michael@0 1129 * Updates the properties of an object previously added using addView.
michael@0 1130 *
michael@0 1131 * @param aView
michael@0 1132 * View object to be updated.
michael@0 1133 */
michael@0 1134 refreshView: function DVP_refreshView(aView)
michael@0 1135 {
michael@0 1136 // Update immediately even if we are still loading data asynchronously.
michael@0 1137 // Subclasses must provide these two functions!
michael@0 1138 this._refreshProperties();
michael@0 1139 this._updateView(aView);
michael@0 1140 },
michael@0 1141
michael@0 1142 /**
michael@0 1143 * Removes an object previously added using addView.
michael@0 1144 *
michael@0 1145 * @param aView
michael@0 1146 * View object to be removed.
michael@0 1147 */
michael@0 1148 removeView: function DVP_removeView(aView)
michael@0 1149 {
michael@0 1150 let index = this._views.indexOf(aView);
michael@0 1151 if (index != -1) {
michael@0 1152 this._views.splice(index, 1);
michael@0 1153 }
michael@0 1154
michael@0 1155 // Stop receiving events when the last of our views is unregistered.
michael@0 1156 if (this._views.length == 0) {
michael@0 1157 if (this._isPrivate) {
michael@0 1158 PrivateDownloadsData.removeView(this);
michael@0 1159 } else {
michael@0 1160 DownloadsData.removeView(this);
michael@0 1161 }
michael@0 1162 }
michael@0 1163 },
michael@0 1164
michael@0 1165 //////////////////////////////////////////////////////////////////////////////
michael@0 1166 //// Callback functions from DownloadsData
michael@0 1167
michael@0 1168 /**
michael@0 1169 * Indicates whether we are still loading downloads data asynchronously.
michael@0 1170 */
michael@0 1171 _loading: false,
michael@0 1172
michael@0 1173 /**
michael@0 1174 * Called before multiple downloads are about to be loaded.
michael@0 1175 */
michael@0 1176 onDataLoadStarting: function DVP_onDataLoadStarting()
michael@0 1177 {
michael@0 1178 this._loading = true;
michael@0 1179 },
michael@0 1180
michael@0 1181 /**
michael@0 1182 * Called after data loading finished.
michael@0 1183 */
michael@0 1184 onDataLoadCompleted: function DVP_onDataLoadCompleted()
michael@0 1185 {
michael@0 1186 this._loading = false;
michael@0 1187 },
michael@0 1188
michael@0 1189 /**
michael@0 1190 * Called when a new download data item is available, either during the
michael@0 1191 * asynchronous data load or when a new download is started.
michael@0 1192 *
michael@0 1193 * @param aDataItem
michael@0 1194 * DownloadsDataItem object that was just added.
michael@0 1195 * @param aNewest
michael@0 1196 * When true, indicates that this item is the most recent and should be
michael@0 1197 * added in the topmost position. This happens when a new download is
michael@0 1198 * started. When false, indicates that the item is the least recent
michael@0 1199 * with regard to the items that have been already added. The latter
michael@0 1200 * generally happens during the asynchronous data load.
michael@0 1201 *
michael@0 1202 * @note Subclasses should override this.
michael@0 1203 */
michael@0 1204 onDataItemAdded: function DVP_onDataItemAdded(aDataItem, aNewest)
michael@0 1205 {
michael@0 1206 throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
michael@0 1207 },
michael@0 1208
michael@0 1209 /**
michael@0 1210 * Called when a data item is removed, ensures that the widget associated with
michael@0 1211 * the view item is removed from the user interface.
michael@0 1212 *
michael@0 1213 * @param aDataItem
michael@0 1214 * DownloadsDataItem object that is being removed.
michael@0 1215 *
michael@0 1216 * @note Subclasses should override this.
michael@0 1217 */
michael@0 1218 onDataItemRemoved: function DVP_onDataItemRemoved(aDataItem)
michael@0 1219 {
michael@0 1220 throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
michael@0 1221 },
michael@0 1222
michael@0 1223 /**
michael@0 1224 * Returns the view item associated with the provided data item for this view.
michael@0 1225 *
michael@0 1226 * @param aDataItem
michael@0 1227 * DownloadsDataItem object for which the view item is requested.
michael@0 1228 *
michael@0 1229 * @return Object that can be used to notify item status events.
michael@0 1230 *
michael@0 1231 * @note Subclasses should override this.
michael@0 1232 */
michael@0 1233 getViewItem: function DID_getViewItem(aDataItem)
michael@0 1234 {
michael@0 1235 throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
michael@0 1236 },
michael@0 1237
michael@0 1238 /**
michael@0 1239 * Private function used to refresh the internal properties being sent to
michael@0 1240 * each registered view.
michael@0 1241 *
michael@0 1242 * @note Subclasses should override this.
michael@0 1243 */
michael@0 1244 _refreshProperties: function DID_refreshProperties()
michael@0 1245 {
michael@0 1246 throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
michael@0 1247 },
michael@0 1248
michael@0 1249 /**
michael@0 1250 * Private function used to refresh an individual view.
michael@0 1251 *
michael@0 1252 * @note Subclasses should override this.
michael@0 1253 */
michael@0 1254 _updateView: function DID_updateView()
michael@0 1255 {
michael@0 1256 throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
michael@0 1257 }
michael@0 1258 };
michael@0 1259
michael@0 1260 ////////////////////////////////////////////////////////////////////////////////
michael@0 1261 //// DownloadsIndicatorData
michael@0 1262
michael@0 1263 /**
michael@0 1264 * This object registers itself with DownloadsData as a view, and transforms the
michael@0 1265 * notifications it receives into overall status data, that is then broadcast to
michael@0 1266 * the registered download status indicators.
michael@0 1267 *
michael@0 1268 * Note that using this object does not automatically start the Download Manager
michael@0 1269 * service. Consumers will see an empty list of downloads until the service is
michael@0 1270 * actually started. This is useful to display a neutral progress indicator in
michael@0 1271 * the main browser window until the autostart timeout elapses.
michael@0 1272 */
michael@0 1273 function DownloadsIndicatorDataCtor(aPrivate) {
michael@0 1274 this._isPrivate = aPrivate;
michael@0 1275 this._views = [];
michael@0 1276 }
michael@0 1277 DownloadsIndicatorDataCtor.prototype = {
michael@0 1278 __proto__: DownloadsViewPrototype,
michael@0 1279
michael@0 1280 /**
michael@0 1281 * Removes an object previously added using addView.
michael@0 1282 *
michael@0 1283 * @param aView
michael@0 1284 * DownloadsIndicatorView object to be removed.
michael@0 1285 */
michael@0 1286 removeView: function DID_removeView(aView)
michael@0 1287 {
michael@0 1288 DownloadsViewPrototype.removeView.call(this, aView);
michael@0 1289
michael@0 1290 if (this._views.length == 0) {
michael@0 1291 this._itemCount = 0;
michael@0 1292 }
michael@0 1293 },
michael@0 1294
michael@0 1295 //////////////////////////////////////////////////////////////////////////////
michael@0 1296 //// Callback functions from DownloadsData
michael@0 1297
michael@0 1298 /**
michael@0 1299 * Called after data loading finished.
michael@0 1300 */
michael@0 1301 onDataLoadCompleted: function DID_onDataLoadCompleted()
michael@0 1302 {
michael@0 1303 DownloadsViewPrototype.onDataLoadCompleted.call(this);
michael@0 1304 this._updateViews();
michael@0 1305 },
michael@0 1306
michael@0 1307 /**
michael@0 1308 * Called when a new download data item is available, either during the
michael@0 1309 * asynchronous data load or when a new download is started.
michael@0 1310 *
michael@0 1311 * @param aDataItem
michael@0 1312 * DownloadsDataItem object that was just added.
michael@0 1313 * @param aNewest
michael@0 1314 * When true, indicates that this item is the most recent and should be
michael@0 1315 * added in the topmost position. This happens when a new download is
michael@0 1316 * started. When false, indicates that the item is the least recent
michael@0 1317 * with regard to the items that have been already added. The latter
michael@0 1318 * generally happens during the asynchronous data load.
michael@0 1319 */
michael@0 1320 onDataItemAdded: function DID_onDataItemAdded(aDataItem, aNewest)
michael@0 1321 {
michael@0 1322 this._itemCount++;
michael@0 1323 this._updateViews();
michael@0 1324 },
michael@0 1325
michael@0 1326 /**
michael@0 1327 * Called when a data item is removed, ensures that the widget associated with
michael@0 1328 * the view item is removed from the user interface.
michael@0 1329 *
michael@0 1330 * @param aDataItem
michael@0 1331 * DownloadsDataItem object that is being removed.
michael@0 1332 */
michael@0 1333 onDataItemRemoved: function DID_onDataItemRemoved(aDataItem)
michael@0 1334 {
michael@0 1335 this._itemCount--;
michael@0 1336 this._updateViews();
michael@0 1337 },
michael@0 1338
michael@0 1339 /**
michael@0 1340 * Returns the view item associated with the provided data item for this view.
michael@0 1341 *
michael@0 1342 * @param aDataItem
michael@0 1343 * DownloadsDataItem object for which the view item is requested.
michael@0 1344 *
michael@0 1345 * @return Object that can be used to notify item status events.
michael@0 1346 */
michael@0 1347 getViewItem: function DID_getViewItem(aDataItem)
michael@0 1348 {
michael@0 1349 let data = this._isPrivate ? PrivateDownloadsIndicatorData
michael@0 1350 : DownloadsIndicatorData;
michael@0 1351 return Object.freeze({
michael@0 1352 onStateChange: function DIVI_onStateChange(aOldState)
michael@0 1353 {
michael@0 1354 if (aDataItem.state == nsIDM.DOWNLOAD_FINISHED ||
michael@0 1355 aDataItem.state == nsIDM.DOWNLOAD_FAILED) {
michael@0 1356 data.attention = true;
michael@0 1357 }
michael@0 1358
michael@0 1359 // Since the state of a download changed, reset the estimated time left.
michael@0 1360 data._lastRawTimeLeft = -1;
michael@0 1361 data._lastTimeLeft = -1;
michael@0 1362
michael@0 1363 data._updateViews();
michael@0 1364 },
michael@0 1365 onProgressChange: function DIVI_onProgressChange()
michael@0 1366 {
michael@0 1367 data._updateViews();
michael@0 1368 }
michael@0 1369 });
michael@0 1370 },
michael@0 1371
michael@0 1372 //////////////////////////////////////////////////////////////////////////////
michael@0 1373 //// Propagation of properties to our views
michael@0 1374
michael@0 1375 // The following properties are updated by _refreshProperties and are then
michael@0 1376 // propagated to the views. See _refreshProperties for details.
michael@0 1377 _hasDownloads: false,
michael@0 1378 _counter: "",
michael@0 1379 _percentComplete: -1,
michael@0 1380 _paused: false,
michael@0 1381
michael@0 1382 /**
michael@0 1383 * Indicates whether the download indicators should be highlighted.
michael@0 1384 */
michael@0 1385 set attention(aValue)
michael@0 1386 {
michael@0 1387 this._attention = aValue;
michael@0 1388 this._updateViews();
michael@0 1389 return aValue;
michael@0 1390 },
michael@0 1391 _attention: false,
michael@0 1392
michael@0 1393 /**
michael@0 1394 * Indicates whether the user is interacting with downloads, thus the
michael@0 1395 * attention indication should not be shown even if requested.
michael@0 1396 */
michael@0 1397 set attentionSuppressed(aValue)
michael@0 1398 {
michael@0 1399 this._attentionSuppressed = aValue;
michael@0 1400 this._attention = false;
michael@0 1401 this._updateViews();
michael@0 1402 return aValue;
michael@0 1403 },
michael@0 1404 _attentionSuppressed: false,
michael@0 1405
michael@0 1406 /**
michael@0 1407 * Computes aggregate values and propagates the changes to our views.
michael@0 1408 */
michael@0 1409 _updateViews: function DID_updateViews()
michael@0 1410 {
michael@0 1411 // Do not update the status indicators during batch loads of download items.
michael@0 1412 if (this._loading) {
michael@0 1413 return;
michael@0 1414 }
michael@0 1415
michael@0 1416 this._refreshProperties();
michael@0 1417 this._views.forEach(this._updateView, this);
michael@0 1418 },
michael@0 1419
michael@0 1420 /**
michael@0 1421 * Updates the specified view with the current aggregate values.
michael@0 1422 *
michael@0 1423 * @param aView
michael@0 1424 * DownloadsIndicatorView object to be updated.
michael@0 1425 */
michael@0 1426 _updateView: function DID_updateView(aView)
michael@0 1427 {
michael@0 1428 aView.hasDownloads = this._hasDownloads;
michael@0 1429 aView.counter = this._counter;
michael@0 1430 aView.percentComplete = this._percentComplete;
michael@0 1431 aView.paused = this._paused;
michael@0 1432 aView.attention = this._attention && !this._attentionSuppressed;
michael@0 1433 },
michael@0 1434
michael@0 1435 //////////////////////////////////////////////////////////////////////////////
michael@0 1436 //// Property updating based on current download status
michael@0 1437
michael@0 1438 /**
michael@0 1439 * Number of download items that are available to be displayed.
michael@0 1440 */
michael@0 1441 _itemCount: 0,
michael@0 1442
michael@0 1443 /**
michael@0 1444 * Floating point value indicating the last number of seconds estimated until
michael@0 1445 * the longest download will finish. We need to store this value so that we
michael@0 1446 * don't continuously apply smoothing if the actual download state has not
michael@0 1447 * changed. This is set to -1 if the previous value is unknown.
michael@0 1448 */
michael@0 1449 _lastRawTimeLeft: -1,
michael@0 1450
michael@0 1451 /**
michael@0 1452 * Last number of seconds estimated until all in-progress downloads with a
michael@0 1453 * known size and speed will finish. This value is stored to allow smoothing
michael@0 1454 * in case of small variations. This is set to -1 if the previous value is
michael@0 1455 * unknown.
michael@0 1456 */
michael@0 1457 _lastTimeLeft: -1,
michael@0 1458
michael@0 1459 /**
michael@0 1460 * A generator function for the dataItems that this summary is currently
michael@0 1461 * interested in. This generator is passed off to summarizeDownloads in order
michael@0 1462 * to generate statistics about the dataItems we care about - in this case,
michael@0 1463 * it's all dataItems for active downloads.
michael@0 1464 */
michael@0 1465 _activeDataItems: function DID_activeDataItems()
michael@0 1466 {
michael@0 1467 let dataItems = this._isPrivate ? PrivateDownloadsData.dataItems
michael@0 1468 : DownloadsData.dataItems;
michael@0 1469 for each (let dataItem in dataItems) {
michael@0 1470 if (dataItem && dataItem.inProgress) {
michael@0 1471 yield dataItem;
michael@0 1472 }
michael@0 1473 }
michael@0 1474 },
michael@0 1475
michael@0 1476 /**
michael@0 1477 * Computes aggregate values based on the current state of downloads.
michael@0 1478 */
michael@0 1479 _refreshProperties: function DID_refreshProperties()
michael@0 1480 {
michael@0 1481 let summary =
michael@0 1482 DownloadsCommon.summarizeDownloads(this._activeDataItems());
michael@0 1483
michael@0 1484 // Determine if the indicator should be shown or get attention.
michael@0 1485 this._hasDownloads = (this._itemCount > 0);
michael@0 1486
michael@0 1487 // If all downloads are paused, show the progress indicator as paused.
michael@0 1488 this._paused = summary.numActive > 0 &&
michael@0 1489 summary.numActive == summary.numPaused;
michael@0 1490
michael@0 1491 this._percentComplete = summary.percentComplete;
michael@0 1492
michael@0 1493 // Display the estimated time left, if present.
michael@0 1494 if (summary.rawTimeLeft == -1) {
michael@0 1495 // There are no downloads with a known time left.
michael@0 1496 this._lastRawTimeLeft = -1;
michael@0 1497 this._lastTimeLeft = -1;
michael@0 1498 this._counter = "";
michael@0 1499 } else {
michael@0 1500 // Compute the new time left only if state actually changed.
michael@0 1501 if (this._lastRawTimeLeft != summary.rawTimeLeft) {
michael@0 1502 this._lastRawTimeLeft = summary.rawTimeLeft;
michael@0 1503 this._lastTimeLeft = DownloadsCommon.smoothSeconds(summary.rawTimeLeft,
michael@0 1504 this._lastTimeLeft);
michael@0 1505 }
michael@0 1506 this._counter = DownloadsCommon.formatTimeLeft(this._lastTimeLeft);
michael@0 1507 }
michael@0 1508 }
michael@0 1509 };
michael@0 1510
michael@0 1511 XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsIndicatorData", function() {
michael@0 1512 return new DownloadsIndicatorDataCtor(true);
michael@0 1513 });
michael@0 1514
michael@0 1515 XPCOMUtils.defineLazyGetter(this, "DownloadsIndicatorData", function() {
michael@0 1516 return new DownloadsIndicatorDataCtor(false);
michael@0 1517 });
michael@0 1518
michael@0 1519 ////////////////////////////////////////////////////////////////////////////////
michael@0 1520 //// DownloadsSummaryData
michael@0 1521
michael@0 1522 /**
michael@0 1523 * DownloadsSummaryData is a view for DownloadsData that produces a summary
michael@0 1524 * of all downloads after a certain exclusion point aNumToExclude. For example,
michael@0 1525 * if there were 5 downloads in progress, and a DownloadsSummaryData was
michael@0 1526 * constructed with aNumToExclude equal to 3, then that DownloadsSummaryData
michael@0 1527 * would produce a summary of the last 2 downloads.
michael@0 1528 *
michael@0 1529 * @param aIsPrivate
michael@0 1530 * True if the browser window which owns the download button is a private
michael@0 1531 * window.
michael@0 1532 * @param aNumToExclude
michael@0 1533 * The number of items to exclude from the summary, starting from the
michael@0 1534 * top of the list.
michael@0 1535 */
michael@0 1536 function DownloadsSummaryData(aIsPrivate, aNumToExclude) {
michael@0 1537 this._numToExclude = aNumToExclude;
michael@0 1538 // Since we can have multiple instances of DownloadsSummaryData, we
michael@0 1539 // override these values from the prototype so that each instance can be
michael@0 1540 // completely separated from one another.
michael@0 1541 this._loading = false;
michael@0 1542
michael@0 1543 this._dataItems = [];
michael@0 1544
michael@0 1545 // Floating point value indicating the last number of seconds estimated until
michael@0 1546 // the longest download will finish. We need to store this value so that we
michael@0 1547 // don't continuously apply smoothing if the actual download state has not
michael@0 1548 // changed. This is set to -1 if the previous value is unknown.
michael@0 1549 this._lastRawTimeLeft = -1;
michael@0 1550
michael@0 1551 // Last number of seconds estimated until all in-progress downloads with a
michael@0 1552 // known size and speed will finish. This value is stored to allow smoothing
michael@0 1553 // in case of small variations. This is set to -1 if the previous value is
michael@0 1554 // unknown.
michael@0 1555 this._lastTimeLeft = -1;
michael@0 1556
michael@0 1557 // The following properties are updated by _refreshProperties and are then
michael@0 1558 // propagated to the views.
michael@0 1559 this._showingProgress = false;
michael@0 1560 this._details = "";
michael@0 1561 this._description = "";
michael@0 1562 this._numActive = 0;
michael@0 1563 this._percentComplete = -1;
michael@0 1564
michael@0 1565 this._isPrivate = aIsPrivate;
michael@0 1566 this._views = [];
michael@0 1567 }
michael@0 1568
michael@0 1569 DownloadsSummaryData.prototype = {
michael@0 1570 __proto__: DownloadsViewPrototype,
michael@0 1571
michael@0 1572 /**
michael@0 1573 * Removes an object previously added using addView.
michael@0 1574 *
michael@0 1575 * @param aView
michael@0 1576 * DownloadsSummary view to be removed.
michael@0 1577 */
michael@0 1578 removeView: function DSD_removeView(aView)
michael@0 1579 {
michael@0 1580 DownloadsViewPrototype.removeView.call(this, aView);
michael@0 1581
michael@0 1582 if (this._views.length == 0) {
michael@0 1583 // Clear out our collection of DownloadDataItems. If we ever have
michael@0 1584 // another view registered with us, this will get re-populated.
michael@0 1585 this._dataItems = [];
michael@0 1586 }
michael@0 1587 },
michael@0 1588
michael@0 1589 //////////////////////////////////////////////////////////////////////////////
michael@0 1590 //// Callback functions from DownloadsData - see the documentation in
michael@0 1591 //// DownloadsViewPrototype for more information on what these functions
michael@0 1592 //// are used for.
michael@0 1593
michael@0 1594 onDataLoadCompleted: function DSD_onDataLoadCompleted()
michael@0 1595 {
michael@0 1596 DownloadsViewPrototype.onDataLoadCompleted.call(this);
michael@0 1597 this._updateViews();
michael@0 1598 },
michael@0 1599
michael@0 1600 onDataItemAdded: function DSD_onDataItemAdded(aDataItem, aNewest)
michael@0 1601 {
michael@0 1602 if (aNewest) {
michael@0 1603 this._dataItems.unshift(aDataItem);
michael@0 1604 } else {
michael@0 1605 this._dataItems.push(aDataItem);
michael@0 1606 }
michael@0 1607
michael@0 1608 this._updateViews();
michael@0 1609 },
michael@0 1610
michael@0 1611 onDataItemRemoved: function DSD_onDataItemRemoved(aDataItem)
michael@0 1612 {
michael@0 1613 let itemIndex = this._dataItems.indexOf(aDataItem);
michael@0 1614 this._dataItems.splice(itemIndex, 1);
michael@0 1615 this._updateViews();
michael@0 1616 },
michael@0 1617
michael@0 1618 getViewItem: function DSD_getViewItem(aDataItem)
michael@0 1619 {
michael@0 1620 let self = this;
michael@0 1621 return Object.freeze({
michael@0 1622 onStateChange: function DIVI_onStateChange(aOldState)
michael@0 1623 {
michael@0 1624 // Since the state of a download changed, reset the estimated time left.
michael@0 1625 self._lastRawTimeLeft = -1;
michael@0 1626 self._lastTimeLeft = -1;
michael@0 1627 self._updateViews();
michael@0 1628 },
michael@0 1629 onProgressChange: function DIVI_onProgressChange()
michael@0 1630 {
michael@0 1631 self._updateViews();
michael@0 1632 }
michael@0 1633 });
michael@0 1634 },
michael@0 1635
michael@0 1636 //////////////////////////////////////////////////////////////////////////////
michael@0 1637 //// Propagation of properties to our views
michael@0 1638
michael@0 1639 /**
michael@0 1640 * Computes aggregate values and propagates the changes to our views.
michael@0 1641 */
michael@0 1642 _updateViews: function DSD_updateViews()
michael@0 1643 {
michael@0 1644 // Do not update the status indicators during batch loads of download items.
michael@0 1645 if (this._loading) {
michael@0 1646 return;
michael@0 1647 }
michael@0 1648
michael@0 1649 this._refreshProperties();
michael@0 1650 this._views.forEach(this._updateView, this);
michael@0 1651 },
michael@0 1652
michael@0 1653 /**
michael@0 1654 * Updates the specified view with the current aggregate values.
michael@0 1655 *
michael@0 1656 * @param aView
michael@0 1657 * DownloadsIndicatorView object to be updated.
michael@0 1658 */
michael@0 1659 _updateView: function DSD_updateView(aView)
michael@0 1660 {
michael@0 1661 aView.showingProgress = this._showingProgress;
michael@0 1662 aView.percentComplete = this._percentComplete;
michael@0 1663 aView.description = this._description;
michael@0 1664 aView.details = this._details;
michael@0 1665 },
michael@0 1666
michael@0 1667 //////////////////////////////////////////////////////////////////////////////
michael@0 1668 //// Property updating based on current download status
michael@0 1669
michael@0 1670 /**
michael@0 1671 * A generator function for the dataItems that this summary is currently
michael@0 1672 * interested in. This generator is passed off to summarizeDownloads in order
michael@0 1673 * to generate statistics about the dataItems we care about - in this case,
michael@0 1674 * it's the dataItems in this._dataItems after the first few to exclude,
michael@0 1675 * which was set when constructing this DownloadsSummaryData instance.
michael@0 1676 */
michael@0 1677 _dataItemsForSummary: function DSD_dataItemsForSummary()
michael@0 1678 {
michael@0 1679 if (this._dataItems.length > 0) {
michael@0 1680 for (let i = this._numToExclude; i < this._dataItems.length; ++i) {
michael@0 1681 yield this._dataItems[i];
michael@0 1682 }
michael@0 1683 }
michael@0 1684 },
michael@0 1685
michael@0 1686 /**
michael@0 1687 * Computes aggregate values based on the current state of downloads.
michael@0 1688 */
michael@0 1689 _refreshProperties: function DSD_refreshProperties()
michael@0 1690 {
michael@0 1691 // Pre-load summary with default values.
michael@0 1692 let summary =
michael@0 1693 DownloadsCommon.summarizeDownloads(this._dataItemsForSummary());
michael@0 1694
michael@0 1695 this._description = DownloadsCommon.strings
michael@0 1696 .otherDownloads2(summary.numActive);
michael@0 1697 this._percentComplete = summary.percentComplete;
michael@0 1698
michael@0 1699 // If all downloads are paused, show the progress indicator as paused.
michael@0 1700 this._showingProgress = summary.numDownloading > 0 ||
michael@0 1701 summary.numPaused > 0;
michael@0 1702
michael@0 1703 // Display the estimated time left, if present.
michael@0 1704 if (summary.rawTimeLeft == -1) {
michael@0 1705 // There are no downloads with a known time left.
michael@0 1706 this._lastRawTimeLeft = -1;
michael@0 1707 this._lastTimeLeft = -1;
michael@0 1708 this._details = "";
michael@0 1709 } else {
michael@0 1710 // Compute the new time left only if state actually changed.
michael@0 1711 if (this._lastRawTimeLeft != summary.rawTimeLeft) {
michael@0 1712 this._lastRawTimeLeft = summary.rawTimeLeft;
michael@0 1713 this._lastTimeLeft = DownloadsCommon.smoothSeconds(summary.rawTimeLeft,
michael@0 1714 this._lastTimeLeft);
michael@0 1715 }
michael@0 1716 [this._details] = DownloadUtils.getDownloadStatusNoRate(
michael@0 1717 summary.totalTransferred, summary.totalSize, summary.slowestSpeed,
michael@0 1718 this._lastTimeLeft);
michael@0 1719 }
michael@0 1720 }
michael@0 1721 }

mercurial