1/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */ 3/* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 5 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7"use strict"; 8 9var EXPORTED_SYMBOLS = ["DownloadsCommon"]; 10 11/** 12 * Handles the Downloads panel shared methods and data access. 13 * 14 * This file includes the following constructors and global objects: 15 * 16 * DownloadsCommon 17 * This object is exposed directly to the consumers of this JavaScript module, 18 * and provides shared methods for all the instances of the user interface. 19 * 20 * DownloadsData 21 * Retrieves the list of past and completed downloads from the underlying 22 * Downloads API data, and provides asynchronous notifications allowing 23 * to build a consistent view of the available data. 24 * 25 * DownloadsIndicatorData 26 * This object registers itself with DownloadsData as a view, and transforms the 27 * notifications it receives into overall status data, that is then broadcast to 28 * the registered download status indicators. 29 */ 30 31// Globals 32 33const { XPCOMUtils } = ChromeUtils.import( 34 "resource://gre/modules/XPCOMUtils.jsm" 35); 36const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 37 38XPCOMUtils.defineLazyModuleGetters(this, { 39 NetUtil: "resource://gre/modules/NetUtil.jsm", 40 PluralForm: "resource://gre/modules/PluralForm.jsm", 41 BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm", 42 DownloadHistory: "resource://gre/modules/DownloadHistory.jsm", 43 Downloads: "resource://gre/modules/Downloads.jsm", 44 DownloadUtils: "resource://gre/modules/DownloadUtils.jsm", 45 PlacesUtils: "resource://gre/modules/PlacesUtils.jsm", 46 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm", 47}); 48 49XPCOMUtils.defineLazyServiceGetters(this, { 50 gClipboardHelper: [ 51 "@mozilla.org/widget/clipboardhelper;1", 52 "nsIClipboardHelper", 53 ], 54 gMIMEService: ["@mozilla.org/mime;1", "nsIMIMEService"], 55}); 56 57XPCOMUtils.defineLazyGetter(this, "DownloadsLogger", () => { 58 let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm"); 59 let consoleOptions = { 60 maxLogLevelPref: "browser.download.loglevel", 61 prefix: "Downloads", 62 }; 63 return new ConsoleAPI(consoleOptions); 64}); 65 66const kDownloadsStringBundleUrl = 67 "chrome://browser/locale/downloads/downloads.properties"; 68 69const kDownloadsStringsRequiringFormatting = { 70 sizeWithUnits: true, 71 statusSeparator: true, 72 statusSeparatorBeforeNumber: true, 73}; 74 75const kDownloadsStringsRequiringPluralForm = { 76 otherDownloads3: true, 77}; 78 79const kMaxHistoryResultsForLimitedView = 42; 80 81const kPrefBranch = Services.prefs.getBranch("browser.download."); 82 83const kGenericContentTypes = [ 84 "application/octet-stream", 85 "binary/octet-stream", 86 "application/unknown", 87]; 88 89var PrefObserver = { 90 QueryInterface: ChromeUtils.generateQI([ 91 "nsIObserver", 92 "nsISupportsWeakReference", 93 ]), 94 getPref(name) { 95 try { 96 switch (typeof this.prefs[name]) { 97 case "boolean": 98 return kPrefBranch.getBoolPref(name); 99 } 100 } catch (ex) {} 101 return this.prefs[name]; 102 }, 103 observe(aSubject, aTopic, aData) { 104 if (this.prefs.hasOwnProperty(aData)) { 105 delete this[aData]; 106 this[aData] = this.getPref(aData); 107 } 108 }, 109 register(prefs) { 110 this.prefs = prefs; 111 kPrefBranch.addObserver("", this, true); 112 for (let key in prefs) { 113 let name = key; 114 XPCOMUtils.defineLazyGetter(this, name, function() { 115 return PrefObserver.getPref(name); 116 }); 117 } 118 }, 119}; 120 121PrefObserver.register({ 122 // prefName: defaultValue 123 animateNotifications: true, 124 openInSystemViewerContextMenuItem: true, 125 alwaysOpenInSystemViewerContextMenuItem: true, 126}); 127 128// DownloadsCommon 129 130/** 131 * This object is exposed directly to the consumers of this JavaScript module, 132 * and provides shared methods for all the instances of the user interface. 133 */ 134var DownloadsCommon = { 135 // The following legacy constants are still returned by stateOfDownload, but 136 // individual properties of the Download object should normally be used. 137 DOWNLOAD_NOTSTARTED: -1, 138 DOWNLOAD_DOWNLOADING: 0, 139 DOWNLOAD_FINISHED: 1, 140 DOWNLOAD_FAILED: 2, 141 DOWNLOAD_CANCELED: 3, 142 DOWNLOAD_PAUSED: 4, 143 DOWNLOAD_BLOCKED_PARENTAL: 6, 144 DOWNLOAD_DIRTY: 8, 145 DOWNLOAD_BLOCKED_POLICY: 9, 146 147 // The following are the possible values of the "attention" property. 148 ATTENTION_NONE: "", 149 ATTENTION_SUCCESS: "success", 150 ATTENTION_WARNING: "warning", 151 ATTENTION_SEVERE: "severe", 152 153 /** 154 * Returns an object whose keys are the string names from the downloads string 155 * bundle, and whose values are either the translated strings or functions 156 * returning formatted strings. 157 */ 158 get strings() { 159 let strings = {}; 160 let sb = Services.strings.createBundle(kDownloadsStringBundleUrl); 161 for (let string of sb.getSimpleEnumeration()) { 162 let stringName = string.key; 163 if (stringName in kDownloadsStringsRequiringFormatting) { 164 strings[stringName] = function() { 165 // Convert "arguments" to a real array before calling into XPCOM. 166 return sb.formatStringFromName(stringName, Array.from(arguments)); 167 }; 168 } else if (stringName in kDownloadsStringsRequiringPluralForm) { 169 strings[stringName] = function(aCount) { 170 // Convert "arguments" to a real array before calling into XPCOM. 171 let formattedString = sb.formatStringFromName( 172 stringName, 173 Array.from(arguments) 174 ); 175 return PluralForm.get(aCount, formattedString); 176 }; 177 } else { 178 strings[stringName] = string.value; 179 } 180 } 181 delete this.strings; 182 return (this.strings = strings); 183 }, 184 185 /** 186 * Indicates whether we should show visual notification on the indicator 187 * when a download event is triggered. 188 */ 189 get animateNotifications() { 190 return PrefObserver.animateNotifications; 191 }, 192 193 /** 194 * Indicates whether or not to show the 'Open in system viewer' context menu item when appropriate 195 */ 196 get openInSystemViewerItemEnabled() { 197 return PrefObserver.openInSystemViewerContextMenuItem; 198 }, 199 200 /** 201 * Indicates whether or not to show the 'Always open...' context menu item when appropriate 202 */ 203 get alwaysOpenInSystemViewerItemEnabled() { 204 return PrefObserver.alwaysOpenInSystemViewerContextMenuItem; 205 }, 206 207 /** 208 * Get access to one of the DownloadsData, PrivateDownloadsData, or 209 * HistoryDownloadsData objects, depending on the privacy status of the 210 * specified window and on whether history downloads should be included. 211 * 212 * @param [optional] window 213 * The browser window which owns the download button. 214 * If not given, the privacy status will be assumed as non-private. 215 * @param [optional] history 216 * True to include history downloads when the window is public. 217 * @param [optional] privateAll 218 * Whether to force the public downloads data to be returned together 219 * with the private downloads data for a private window. 220 * @param [optional] limited 221 * True to limit the amount of downloads returned to 222 * `kMaxHistoryResultsForLimitedView`. 223 */ 224 getData(window, history = false, privateAll = false, limited = false) { 225 let isPrivate = 226 window && PrivateBrowsingUtils.isContentWindowPrivate(window); 227 if (isPrivate && !privateAll) { 228 return PrivateDownloadsData; 229 } 230 if (history) { 231 if (isPrivate && privateAll) { 232 return LimitedPrivateHistoryDownloadData; 233 } 234 return limited ? LimitedHistoryDownloadsData : HistoryDownloadsData; 235 } 236 return DownloadsData; 237 }, 238 239 /** 240 * Initializes the Downloads back-end and starts receiving events for both the 241 * private and non-private downloads data objects. 242 */ 243 initializeAllDataLinks() { 244 DownloadsData.initializeDataLink(); 245 PrivateDownloadsData.initializeDataLink(); 246 }, 247 248 /** 249 * Get access to one of the DownloadsIndicatorData or 250 * PrivateDownloadsIndicatorData objects, depending on the privacy status of 251 * the window in question. 252 */ 253 getIndicatorData(aWindow) { 254 if (PrivateBrowsingUtils.isContentWindowPrivate(aWindow)) { 255 return PrivateDownloadsIndicatorData; 256 } 257 return DownloadsIndicatorData; 258 }, 259 260 /** 261 * Returns a reference to the DownloadsSummaryData singleton - creating one 262 * in the process if one hasn't been instantiated yet. 263 * 264 * @param aWindow 265 * The browser window which owns the download button. 266 * @param aNumToExclude 267 * The number of items on the top of the downloads list to exclude 268 * from the summary. 269 */ 270 getSummary(aWindow, aNumToExclude) { 271 if (PrivateBrowsingUtils.isContentWindowPrivate(aWindow)) { 272 if (this._privateSummary) { 273 return this._privateSummary; 274 } 275 return (this._privateSummary = new DownloadsSummaryData( 276 true, 277 aNumToExclude 278 )); 279 } 280 if (this._summary) { 281 return this._summary; 282 } 283 return (this._summary = new DownloadsSummaryData(false, aNumToExclude)); 284 }, 285 _summary: null, 286 _privateSummary: null, 287 288 /** 289 * Returns the legacy state integer value for the provided Download object. 290 */ 291 stateOfDownload(download) { 292 // Collapse state using the correct priority. 293 if (!download.stopped) { 294 return DownloadsCommon.DOWNLOAD_DOWNLOADING; 295 } 296 if (download.succeeded) { 297 return DownloadsCommon.DOWNLOAD_FINISHED; 298 } 299 if (download.error) { 300 if (download.error.becauseBlockedByParentalControls) { 301 return DownloadsCommon.DOWNLOAD_BLOCKED_PARENTAL; 302 } 303 if (download.error.becauseBlockedByReputationCheck) { 304 return DownloadsCommon.DOWNLOAD_DIRTY; 305 } 306 return DownloadsCommon.DOWNLOAD_FAILED; 307 } 308 if (download.canceled) { 309 if (download.hasPartialData) { 310 return DownloadsCommon.DOWNLOAD_PAUSED; 311 } 312 return DownloadsCommon.DOWNLOAD_CANCELED; 313 } 314 return DownloadsCommon.DOWNLOAD_NOTSTARTED; 315 }, 316 317 /** 318 * Removes a Download object from both session and history downloads. 319 */ 320 async deleteDownload(download) { 321 // Remove the associated history element first, if any, so that the views 322 // that combine history and session downloads won't resurrect the history 323 // download into the view just before it is deleted permanently. 324 try { 325 await PlacesUtils.history.remove(download.source.url); 326 } catch (ex) { 327 Cu.reportError(ex); 328 } 329 let list = await Downloads.getList(Downloads.ALL); 330 await list.remove(download); 331 await download.finalize(true); 332 }, 333 334 /** 335 * Get a nsIMIMEInfo object for a download 336 */ 337 getMimeInfo(download) { 338 if (!download.succeeded) { 339 return null; 340 } 341 let contentType = download.contentType; 342 let url = Cc["@mozilla.org/network/standard-url-mutator;1"] 343 .createInstance(Ci.nsIURIMutator) 344 .setSpec("http://example.com") // construct the URL 345 .setFilePath(download.target.path) 346 .finalize() 347 .QueryInterface(Ci.nsIURL); 348 let fileExtension = url.fileExtension; 349 350 // look at file extension if there's no contentType or it is generic 351 if (!contentType || kGenericContentTypes.includes(contentType)) { 352 try { 353 contentType = gMIMEService.getTypeFromExtension(fileExtension); 354 } catch (ex) { 355 DownloadsCommon.log( 356 "Cant get mimeType from file extension: ", 357 fileExtension 358 ); 359 } 360 } 361 if (!(contentType || fileExtension)) { 362 return null; 363 } 364 let mimeInfo = null; 365 try { 366 mimeInfo = gMIMEService.getFromTypeAndExtension( 367 contentType || "", 368 fileExtension || "" 369 ); 370 } catch (ex) { 371 DownloadsCommon.log( 372 "Can't get nsIMIMEInfo for contentType: ", 373 contentType, 374 "and fileExtension:", 375 fileExtension 376 ); 377 } 378 return mimeInfo; 379 }, 380 381 /** 382 * Confirm if the download exists on the filesystem and is a given mime-type 383 */ 384 isFileOfType(download, mimeType) { 385 if (!(download.succeeded && download.target?.exists)) { 386 DownloadsCommon.log( 387 `isFileOfType returning false for mimeType: ${mimeType}, succeeded: ${download.succeeded}, exists: ${download.target?.exists}` 388 ); 389 return false; 390 } 391 let mimeInfo = DownloadsCommon.getMimeInfo(download); 392 return mimeInfo?.type === mimeType.toLowerCase(); 393 }, 394 395 /** 396 * Copies the source URI of the given Download object to the clipboard. 397 */ 398 copyDownloadLink(download) { 399 gClipboardHelper.copyString(download.source.url); 400 }, 401 402 /** 403 * Given an iterable collection of Download objects, generates and returns 404 * statistics about that collection. 405 * 406 * @param downloads An iterable collection of Download objects. 407 * 408 * @return Object whose properties are the generated statistics. Currently, 409 * we return the following properties: 410 * 411 * numActive : The total number of downloads. 412 * numPaused : The total number of paused downloads. 413 * numDownloading : The total number of downloads being downloaded. 414 * totalSize : The total size of all downloads once completed. 415 * totalTransferred: The total amount of transferred data for these 416 * downloads. 417 * slowestSpeed : The slowest download rate. 418 * rawTimeLeft : The estimated time left for the downloads to 419 * complete. 420 * percentComplete : The percentage of bytes successfully downloaded. 421 */ 422 summarizeDownloads(downloads) { 423 let summary = { 424 numActive: 0, 425 numPaused: 0, 426 numDownloading: 0, 427 totalSize: 0, 428 totalTransferred: 0, 429 // slowestSpeed is Infinity so that we can use Math.min to 430 // find the slowest speed. We'll set this to 0 afterwards if 431 // it's still at Infinity by the time we're done iterating all 432 // download. 433 slowestSpeed: Infinity, 434 rawTimeLeft: -1, 435 percentComplete: -1, 436 }; 437 438 for (let download of downloads) { 439 summary.numActive++; 440 441 if (!download.stopped) { 442 summary.numDownloading++; 443 if (download.hasProgress && download.speed > 0) { 444 let sizeLeft = download.totalBytes - download.currentBytes; 445 summary.rawTimeLeft = Math.max( 446 summary.rawTimeLeft, 447 sizeLeft / download.speed 448 ); 449 summary.slowestSpeed = Math.min(summary.slowestSpeed, download.speed); 450 } 451 } else if (download.canceled && download.hasPartialData) { 452 summary.numPaused++; 453 } 454 455 // Only add to total values if we actually know the download size. 456 if (download.succeeded) { 457 summary.totalSize += download.target.size; 458 summary.totalTransferred += download.target.size; 459 } else if (download.hasProgress) { 460 summary.totalSize += download.totalBytes; 461 summary.totalTransferred += download.currentBytes; 462 } 463 } 464 465 if (summary.totalSize != 0) { 466 summary.percentComplete = Math.floor( 467 (summary.totalTransferred / summary.totalSize) * 100 468 ); 469 } 470 471 if (summary.slowestSpeed == Infinity) { 472 summary.slowestSpeed = 0; 473 } 474 475 return summary; 476 }, 477 478 /** 479 * If necessary, smooths the estimated number of seconds remaining for one 480 * or more downloads to complete. 481 * 482 * @param aSeconds 483 * Current raw estimate on number of seconds left for one or more 484 * downloads. This is a floating point value to help get sub-second 485 * accuracy for current and future estimates. 486 */ 487 smoothSeconds(aSeconds, aLastSeconds) { 488 // We apply an algorithm similar to the DownloadUtils.getTimeLeft function, 489 // though tailored to a single time estimation for all downloads. We never 490 // apply something if the new value is less than half the previous value. 491 let shouldApplySmoothing = aLastSeconds >= 0 && aSeconds > aLastSeconds / 2; 492 if (shouldApplySmoothing) { 493 // Apply hysteresis to favor downward over upward swings. Trust only 30% 494 // of the new value if lower, and 10% if higher (exponential smoothing). 495 let diff = aSeconds - aLastSeconds; 496 aSeconds = aLastSeconds + (diff < 0 ? 0.3 : 0.1) * diff; 497 498 // If the new time is similar, reuse something close to the last time 499 // left, but subtract a little to provide forward progress. 500 diff = aSeconds - aLastSeconds; 501 let diffPercent = (diff / aLastSeconds) * 100; 502 if (Math.abs(diff) < 5 || Math.abs(diffPercent) < 5) { 503 aSeconds = aLastSeconds - (diff < 0 ? 0.4 : 0.2); 504 } 505 } 506 507 // In the last few seconds of downloading, we are always subtracting and 508 // never adding to the time left. Ensure that we never fall below one 509 // second left until all downloads are actually finished. 510 return (aLastSeconds = Math.max(aSeconds, 1)); 511 }, 512 513 /** 514 * Opens a downloaded file. 515 * 516 * @param downloadProperties 517 * A Download object or the initial properties of a serialized download 518 * @param options.openWhere 519 * Optional string indicating how to handle opening a download target file URI. 520 * One of "window", "tab", "tabshifted". 521 * @param options.useSystemDefault 522 * Optional value indicating how to handle launching this download, 523 * this call only. Will override the associated mimeInfo.preferredAction 524 * @return {Promise} 525 * @resolves When the instruction to launch the file has been 526 * successfully given to the operating system or handled internally 527 * @rejects JavaScript exception if there was an error trying to launch 528 * the file. 529 */ 530 async openDownload(download, options) { 531 // some download objects got serialized and need reconstituting 532 if (typeof download.launch !== "function") { 533 download = await Downloads.createDownload(download); 534 } 535 return download.launch(options).catch(ex => Cu.reportError(ex)); 536 }, 537 538 /** 539 * Show a downloaded file in the system file manager. 540 * 541 * @param aFile 542 * a downloaded file. 543 */ 544 showDownloadedFile(aFile) { 545 if (!(aFile instanceof Ci.nsIFile)) { 546 throw new Error("aFile must be a nsIFile object"); 547 } 548 try { 549 // Show the directory containing the file and select the file. 550 aFile.reveal(); 551 } catch (ex) { 552 // If reveal fails for some reason (e.g., it's not implemented on unix 553 // or the file doesn't exist), try using the parent if we have it. 554 let parent = aFile.parent; 555 if (parent) { 556 this.showDirectory(parent); 557 } 558 } 559 }, 560 561 /** 562 * Show the specified folder in the system file manager. 563 * 564 * @param aDirectory 565 * a directory to be opened with system file manager. 566 */ 567 showDirectory(aDirectory) { 568 if (!(aDirectory instanceof Ci.nsIFile)) { 569 throw new Error("aDirectory must be a nsIFile object"); 570 } 571 try { 572 aDirectory.launch(); 573 } catch (ex) { 574 // If launch fails (probably because it's not implemented), let 575 // the OS handler try to open the directory. 576 Cc["@mozilla.org/uriloader/external-protocol-service;1"] 577 .getService(Ci.nsIExternalProtocolService) 578 .loadURI( 579 NetUtil.newURI(aDirectory), 580 Services.scriptSecurityManager.getSystemPrincipal() 581 ); 582 } 583 }, 584 585 /** 586 * Displays an alert message box which asks the user if they want to 587 * unblock the downloaded file or not. 588 * 589 * @param options 590 * An object with the following properties: 591 * { 592 * verdict: 593 * The detailed reason why the download was blocked, according to 594 * the "Downloads.Error.BLOCK_VERDICT_" constants. If an unknown 595 * reason is specified, "Downloads.Error.BLOCK_VERDICT_MALWARE" is 596 * assumed. 597 * window: 598 * The window with which this action is associated. 599 * dialogType: 600 * String that determines which actions are available: 601 * - "unblock" to offer just "unblock". 602 * - "chooseUnblock" to offer "unblock" and "confirmBlock". 603 * - "chooseOpen" to offer "open" and "confirmBlock". 604 * } 605 * 606 * @return {Promise} 607 * @resolves String representing the action that should be executed: 608 * - "open" to allow the download and open the file. 609 * - "unblock" to allow the download without opening the file. 610 * - "confirmBlock" to delete the blocked data permanently. 611 * - "cancel" to do nothing and cancel the operation. 612 */ 613 async confirmUnblockDownload({ verdict, window, dialogType }) { 614 let s = DownloadsCommon.strings; 615 616 // All the dialogs have an action button and a cancel button, while only 617 // some of them have an additonal button to remove the file. The cancel 618 // button must always be the one at BUTTON_POS_1 because this is the value 619 // returned by confirmEx when using ESC or closing the dialog (bug 345067). 620 let title = s.unblockHeaderUnblock; 621 let firstButtonText = s.unblockButtonUnblock; 622 let firstButtonAction = "unblock"; 623 let buttonFlags = 624 Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0 + 625 Ci.nsIPrompt.BUTTON_TITLE_CANCEL * Ci.nsIPrompt.BUTTON_POS_1; 626 627 switch (dialogType) { 628 case "unblock": 629 // Use only the unblock action. The default is to cancel. 630 buttonFlags += Ci.nsIPrompt.BUTTON_POS_1_DEFAULT; 631 break; 632 case "chooseUnblock": 633 // Use the unblock and remove file actions. The default is remove file. 634 buttonFlags += 635 Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2 + 636 Ci.nsIPrompt.BUTTON_POS_2_DEFAULT; 637 break; 638 case "chooseOpen": 639 // Use the unblock and open file actions. The default is open file. 640 title = s.unblockHeaderOpen; 641 firstButtonText = s.unblockButtonOpen; 642 firstButtonAction = "open"; 643 buttonFlags += 644 Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2 + 645 Ci.nsIPrompt.BUTTON_POS_0_DEFAULT; 646 break; 647 default: 648 Cu.reportError("Unexpected dialog type: " + dialogType); 649 return "cancel"; 650 } 651 652 let message; 653 switch (verdict) { 654 case Downloads.Error.BLOCK_VERDICT_UNCOMMON: 655 message = s.unblockTypeUncommon2; 656 break; 657 case Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED: 658 message = s.unblockTypePotentiallyUnwanted2; 659 break; 660 default: 661 // Assume Downloads.Error.BLOCK_VERDICT_MALWARE 662 message = s.unblockTypeMalware; 663 break; 664 } 665 message += "\n\n" + s.unblockTip2; 666 667 Services.ww.registerNotification(function onOpen(subj, topic) { 668 if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) { 669 // Make sure to listen for "DOMContentLoaded" because it is fired 670 // before the "load" event. 671 subj.addEventListener( 672 "DOMContentLoaded", 673 function() { 674 if ( 675 subj.document.documentURI == 676 "chrome://global/content/commonDialog.xhtml" 677 ) { 678 Services.ww.unregisterNotification(onOpen); 679 let dialog = subj.document.getElementById("commonDialog"); 680 if (dialog) { 681 // Change the dialog to use a warning icon. 682 dialog.classList.add("alert-dialog"); 683 } 684 } 685 }, 686 { once: true } 687 ); 688 } 689 }); 690 691 let rv = Services.prompt.confirmEx( 692 window, 693 title, 694 message, 695 buttonFlags, 696 firstButtonText, 697 null, 698 s.unblockButtonConfirmBlock, 699 null, 700 {} 701 ); 702 return [firstButtonAction, "cancel", "confirmBlock"][rv]; 703 }, 704}; 705 706XPCOMUtils.defineLazyGetter(DownloadsCommon, "log", () => { 707 return DownloadsLogger.log.bind(DownloadsLogger); 708}); 709XPCOMUtils.defineLazyGetter(DownloadsCommon, "error", () => { 710 return DownloadsLogger.error.bind(DownloadsLogger); 711}); 712 713/** 714 * Returns true if we are executing on Windows Vista or a later version. 715 */ 716XPCOMUtils.defineLazyGetter(DownloadsCommon, "isWinVistaOrHigher", function() { 717 let os = Services.appinfo.OS; 718 if (os != "WINNT") { 719 return false; 720 } 721 return parseFloat(Services.sysinfo.getProperty("version")) >= 6; 722}); 723 724// DownloadsData 725 726/** 727 * Retrieves the list of past and completed downloads from the underlying 728 * Downloads API data, and provides asynchronous notifications allowing to 729 * build a consistent view of the available data. 730 * 731 * Note that using this object does not automatically initialize the list of 732 * downloads. This is useful to display a neutral progress indicator in 733 * the main browser window until the autostart timeout elapses. 734 * 735 * This powers the DownloadsData, PrivateDownloadsData, and HistoryDownloadsData 736 * singleton objects. 737 */ 738function DownloadsDataCtor({ isPrivate, isHistory, maxHistoryResults } = {}) { 739 this._isPrivate = !!isPrivate; 740 741 // Contains all the available Download objects and their integer state. 742 this.oldDownloadStates = new Map(); 743 744 // For the history downloads list we don't need to register this as a view, 745 // but we have to ensure that the DownloadsData object is initialized before 746 // we register more views. This ensures that the view methods of DownloadsData 747 // are invoked before those of views registered on HistoryDownloadsData, 748 // allowing the endTime property to be set correctly. 749 if (isHistory) { 750 if (isPrivate) { 751 PrivateDownloadsData.initializeDataLink(); 752 } 753 DownloadsData.initializeDataLink(); 754 this._promiseList = DownloadsData._promiseList.then(() => { 755 // For history downloads in Private Browsing mode, we'll fetch the combined 756 // list of public and private downloads. 757 return DownloadHistory.getList({ 758 type: isPrivate ? Downloads.ALL : Downloads.PUBLIC, 759 maxHistoryResults, 760 }); 761 }); 762 return; 763 } 764 765 // This defines "initializeDataLink" and "_promiseList" synchronously, then 766 // continues execution only when "initializeDataLink" is called, allowing the 767 // underlying data to be loaded only when actually needed. 768 this._promiseList = (async () => { 769 await new Promise(resolve => (this.initializeDataLink = resolve)); 770 let list = await Downloads.getList( 771 isPrivate ? Downloads.PRIVATE : Downloads.PUBLIC 772 ); 773 await list.addView(this); 774 return list; 775 })(); 776} 777 778DownloadsDataCtor.prototype = { 779 /** 780 * Starts receiving events for current downloads. 781 */ 782 initializeDataLink() {}, 783 784 /** 785 * Promise resolved with the underlying DownloadList object once we started 786 * receiving events for current downloads. 787 */ 788 _promiseList: null, 789 790 /** 791 * Iterator for all the available Download objects. This is empty until the 792 * data has been loaded using the JavaScript API for downloads. 793 */ 794 get downloads() { 795 return this.oldDownloadStates.keys(); 796 }, 797 798 /** 799 * True if there are finished downloads that can be removed from the list. 800 */ 801 get canRemoveFinished() { 802 for (let download of this.downloads) { 803 // Stopped, paused, and failed downloads with partial data are removed. 804 if (download.stopped && !(download.canceled && download.hasPartialData)) { 805 return true; 806 } 807 } 808 return false; 809 }, 810 811 /** 812 * Asks the back-end to remove finished downloads from the list. This method 813 * is only called after the data link has been initialized. 814 */ 815 removeFinished() { 816 Downloads.getList(this._isPrivate ? Downloads.PRIVATE : Downloads.PUBLIC) 817 .then(list => list.removeFinished()) 818 .catch(Cu.reportError); 819 let indicatorData = this._isPrivate 820 ? PrivateDownloadsIndicatorData 821 : DownloadsIndicatorData; 822 indicatorData.attention = DownloadsCommon.ATTENTION_NONE; 823 }, 824 825 // Integration with the asynchronous Downloads back-end 826 827 onDownloadAdded(download) { 828 // Download objects do not store the end time of downloads, as the Downloads 829 // API does not need to persist this information for all platforms. Once a 830 // download terminates on a Desktop browser, it becomes a history download, 831 // for which the end time is stored differently, as a Places annotation. 832 download.endTime = Date.now(); 833 834 this.oldDownloadStates.set( 835 download, 836 DownloadsCommon.stateOfDownload(download) 837 ); 838 if (download.error?.becauseBlockedByReputationCheck) { 839 this._notifyDownloadEvent("error"); 840 } 841 }, 842 843 onDownloadChanged(download) { 844 let oldState = this.oldDownloadStates.get(download); 845 let newState = DownloadsCommon.stateOfDownload(download); 846 this.oldDownloadStates.set(download, newState); 847 848 if (oldState != newState) { 849 if ( 850 download.succeeded || 851 (download.canceled && !download.hasPartialData) || 852 download.error 853 ) { 854 // Store the end time that may be displayed by the views. 855 download.endTime = Date.now(); 856 857 // This state transition code should actually be located in a Downloads 858 // API module (bug 941009). 859 DownloadHistory.updateMetaData(download).catch(Cu.reportError); 860 } 861 862 if ( 863 download.succeeded || 864 (download.error && download.error.becauseBlocked) 865 ) { 866 this._notifyDownloadEvent("finish"); 867 } 868 } 869 870 if (!download.newDownloadNotified) { 871 download.newDownloadNotified = true; 872 this._notifyDownloadEvent("start"); 873 } 874 }, 875 876 onDownloadRemoved(download) { 877 this.oldDownloadStates.delete(download); 878 }, 879 880 // Registration of views 881 882 /** 883 * Adds an object to be notified when the available download data changes. 884 * The specified object is initialized with the currently available downloads. 885 * 886 * @param aView 887 * DownloadsView object to be added. This reference must be passed to 888 * removeView before termination. 889 */ 890 addView(aView) { 891 this._promiseList.then(list => list.addView(aView)).catch(Cu.reportError); 892 }, 893 894 /** 895 * Removes an object previously added using addView. 896 * 897 * @param aView 898 * DownloadsView object to be removed. 899 */ 900 removeView(aView) { 901 this._promiseList 902 .then(list => list.removeView(aView)) 903 .catch(Cu.reportError); 904 }, 905 906 // Notifications sent to the most recent browser window only 907 908 /** 909 * Set to true after the first download causes the downloads panel to be 910 * displayed. 911 */ 912 get panelHasShownBefore() { 913 try { 914 return Services.prefs.getBoolPref("browser.download.panel.shown"); 915 } catch (ex) {} 916 return false; 917 }, 918 919 set panelHasShownBefore(aValue) { 920 Services.prefs.setBoolPref("browser.download.panel.shown", aValue); 921 }, 922 923 /** 924 * Displays a new or finished download notification in the most recent browser 925 * window, if one is currently available with the required privacy type. 926 * 927 * @param aType 928 * Set to "start" for new downloads, "finish" for completed downloads, 929 * "error" for downloads that failed and need attention 930 */ 931 _notifyDownloadEvent(aType) { 932 DownloadsCommon.log( 933 "Attempting to notify that a new download has started or finished." 934 ); 935 936 // Show the panel in the most recent browser window, if present. 937 let browserWin = BrowserWindowTracker.getTopWindow({ 938 private: this._isPrivate, 939 }); 940 if (!browserWin) { 941 return; 942 } 943 944 let shouldOpenDownloadsPanel = 945 aType == "start" && 946 Services.prefs.getBoolPref( 947 "browser.download.improvements_to_download_panel" 948 ) && 949 DownloadsCommon.summarizeDownloads(this.downloads).numDownloading <= 1; 950 951 if ( 952 this.panelHasShownBefore && 953 aType != "error" && 954 !shouldOpenDownloadsPanel 955 ) { 956 // For new downloads after the first one, don't show the panel 957 // automatically, but provide a visible notification in the topmost 958 // browser window, if the status indicator is already visible. 959 DownloadsCommon.log("Showing new download notification."); 960 browserWin.DownloadsIndicatorView.showEventNotification(aType); 961 return; 962 } 963 this.panelHasShownBefore = true; 964 browserWin.DownloadsPanel.showPanel(); 965 }, 966}; 967 968XPCOMUtils.defineLazyGetter(this, "HistoryDownloadsData", function() { 969 return new DownloadsDataCtor({ isHistory: true }); 970}); 971 972XPCOMUtils.defineLazyGetter(this, "LimitedHistoryDownloadsData", function() { 973 return new DownloadsDataCtor({ 974 isHistory: true, 975 maxHistoryResults: kMaxHistoryResultsForLimitedView, 976 }); 977}); 978 979XPCOMUtils.defineLazyGetter( 980 this, 981 "LimitedPrivateHistoryDownloadData", 982 function() { 983 return new DownloadsDataCtor({ 984 isPrivate: true, 985 isHistory: true, 986 maxHistoryResults: kMaxHistoryResultsForLimitedView, 987 }); 988 } 989); 990 991XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsData", function() { 992 return new DownloadsDataCtor({ isPrivate: true }); 993}); 994 995XPCOMUtils.defineLazyGetter(this, "DownloadsData", function() { 996 return new DownloadsDataCtor(); 997}); 998 999// DownloadsViewPrototype 1000 1001/** 1002 * A prototype for an object that registers itself with DownloadsData as soon 1003 * as a view is registered with it. 1004 */ 1005const DownloadsViewPrototype = { 1006 /** 1007 * Contains all the available Download objects and their current state value. 1008 * 1009 * SUBCLASSES MUST OVERRIDE THIS PROPERTY. 1010 */ 1011 _oldDownloadStates: null, 1012 1013 // Registration of views 1014 1015 /** 1016 * Array of view objects that should be notified when the available status 1017 * data changes. 1018 * 1019 * SUBCLASSES MUST OVERRIDE THIS PROPERTY. 1020 */ 1021 _views: null, 1022 1023 /** 1024 * Determines whether this view object is over the private or non-private 1025 * downloads. 1026 * 1027 * SUBCLASSES MUST OVERRIDE THIS PROPERTY. 1028 */ 1029 _isPrivate: false, 1030 1031 /** 1032 * Adds an object to be notified when the available status data changes. 1033 * The specified object is initialized with the currently available status. 1034 * 1035 * @param aView 1036 * View object to be added. This reference must be 1037 * passed to removeView before termination. 1038 */ 1039 addView(aView) { 1040 // Start receiving events when the first of our views is registered. 1041 if (!this._views.length) { 1042 if (this._isPrivate) { 1043 PrivateDownloadsData.addView(this); 1044 } else { 1045 DownloadsData.addView(this); 1046 } 1047 } 1048 1049 this._views.push(aView); 1050 this.refreshView(aView); 1051 }, 1052 1053 /** 1054 * Updates the properties of an object previously added using addView. 1055 * 1056 * @param aView 1057 * View object to be updated. 1058 */ 1059 refreshView(aView) { 1060 // Update immediately even if we are still loading data asynchronously. 1061 // Subclasses must provide these two functions! 1062 this._refreshProperties(); 1063 this._updateView(aView); 1064 }, 1065 1066 /** 1067 * Removes an object previously added using addView. 1068 * 1069 * @param aView 1070 * View object to be removed. 1071 */ 1072 removeView(aView) { 1073 let index = this._views.indexOf(aView); 1074 if (index != -1) { 1075 this._views.splice(index, 1); 1076 } 1077 1078 // Stop receiving events when the last of our views is unregistered. 1079 if (!this._views.length) { 1080 if (this._isPrivate) { 1081 PrivateDownloadsData.removeView(this); 1082 } else { 1083 DownloadsData.removeView(this); 1084 } 1085 } 1086 }, 1087 1088 // Callback functions from DownloadList 1089 1090 /** 1091 * Indicates whether we are still loading downloads data asynchronously. 1092 */ 1093 _loading: false, 1094 1095 /** 1096 * Called before multiple downloads are about to be loaded. 1097 */ 1098 onDownloadBatchStarting() { 1099 this._loading = true; 1100 }, 1101 1102 /** 1103 * Called after data loading finished. 1104 */ 1105 onDownloadBatchEnded() { 1106 this._loading = false; 1107 this._updateViews(); 1108 }, 1109 1110 /** 1111 * Called when a new download data item is available, either during the 1112 * asynchronous data load or when a new download is started. 1113 * 1114 * @param download 1115 * Download object that was just added. 1116 * 1117 * @note Subclasses should override this and still call the base method. 1118 */ 1119 onDownloadAdded(download) { 1120 this._oldDownloadStates.set( 1121 download, 1122 DownloadsCommon.stateOfDownload(download) 1123 ); 1124 }, 1125 1126 /** 1127 * Called when the overall state of a Download has changed. In particular, 1128 * this is called only once when the download succeeds or is blocked 1129 * permanently, and is never called if only the current progress changed. 1130 * 1131 * The onDownloadChanged notification will always be sent afterwards. 1132 * 1133 * @note Subclasses should override this. 1134 */ 1135 onDownloadStateChanged(download) { 1136 throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); 1137 }, 1138 1139 /** 1140 * Called every time any state property of a Download may have changed, 1141 * including progress properties. 1142 * 1143 * Note that progress notification changes are throttled at the Downloads.jsm 1144 * API level, and there is no throttling mechanism in the front-end. 1145 * 1146 * @note Subclasses should override this and still call the base method. 1147 */ 1148 onDownloadChanged(download) { 1149 let oldState = this._oldDownloadStates.get(download); 1150 let newState = DownloadsCommon.stateOfDownload(download); 1151 this._oldDownloadStates.set(download, newState); 1152 1153 if (oldState != newState) { 1154 this.onDownloadStateChanged(download); 1155 } 1156 }, 1157 1158 /** 1159 * Called when a data item is removed, ensures that the widget associated with 1160 * the view item is removed from the user interface. 1161 * 1162 * @param download 1163 * Download object that is being removed. 1164 * 1165 * @note Subclasses should override this. 1166 */ 1167 onDownloadRemoved(download) { 1168 throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); 1169 }, 1170 1171 /** 1172 * Private function used to refresh the internal properties being sent to 1173 * each registered view. 1174 * 1175 * @note Subclasses should override this. 1176 */ 1177 _refreshProperties() { 1178 throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); 1179 }, 1180 1181 /** 1182 * Private function used to refresh an individual view. 1183 * 1184 * @note Subclasses should override this. 1185 */ 1186 _updateView() { 1187 throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED); 1188 }, 1189 1190 /** 1191 * Computes aggregate values and propagates the changes to our views. 1192 */ 1193 _updateViews() { 1194 // Do not update the status indicators during batch loads of download items. 1195 if (this._loading) { 1196 return; 1197 } 1198 1199 this._refreshProperties(); 1200 this._views.forEach(this._updateView, this); 1201 }, 1202}; 1203 1204// DownloadsIndicatorData 1205 1206/** 1207 * This object registers itself with DownloadsData as a view, and transforms the 1208 * notifications it receives into overall status data, that is then broadcast to 1209 * the registered download status indicators. 1210 * 1211 * Note that using this object does not automatically start the Download Manager 1212 * service. Consumers will see an empty list of downloads until the service is 1213 * actually started. This is useful to display a neutral progress indicator in 1214 * the main browser window until the autostart timeout elapses. 1215 */ 1216function DownloadsIndicatorDataCtor(aPrivate) { 1217 this._oldDownloadStates = new WeakMap(); 1218 this._isPrivate = aPrivate; 1219 this._views = []; 1220} 1221DownloadsIndicatorDataCtor.prototype = { 1222 __proto__: DownloadsViewPrototype, 1223 1224 /** 1225 * Removes an object previously added using addView. 1226 * 1227 * @param aView 1228 * DownloadsIndicatorView object to be removed. 1229 */ 1230 removeView(aView) { 1231 DownloadsViewPrototype.removeView.call(this, aView); 1232 1233 if (!this._views.length) { 1234 this._itemCount = 0; 1235 } 1236 }, 1237 1238 onDownloadAdded(download) { 1239 DownloadsViewPrototype.onDownloadAdded.call(this, download); 1240 this._itemCount++; 1241 this._updateViews(); 1242 }, 1243 1244 onDownloadStateChanged(download) { 1245 if ( 1246 !download.succeeded && 1247 download.error && 1248 download.error.reputationCheckVerdict 1249 ) { 1250 switch (download.error.reputationCheckVerdict) { 1251 case Downloads.Error.BLOCK_VERDICT_UNCOMMON: // fall-through 1252 case Downloads.Error.BLOCK_VERDICT_POTENTIALLY_UNWANTED: 1253 case Downloads.Error.BLOCK_VERDICT_INSECURE: 1254 // Existing higher level attention indication trumps ATTENTION_WARNING. 1255 if (this._attention != DownloadsCommon.ATTENTION_SEVERE) { 1256 this.attention = DownloadsCommon.ATTENTION_WARNING; 1257 } 1258 break; 1259 case Downloads.Error.BLOCK_VERDICT_MALWARE: 1260 this.attention = DownloadsCommon.ATTENTION_SEVERE; 1261 break; 1262 default: 1263 this.attention = DownloadsCommon.ATTENTION_SEVERE; 1264 Cu.reportError( 1265 "Unknown reputation verdict: " + 1266 download.error.reputationCheckVerdict 1267 ); 1268 } 1269 } else if (download.succeeded) { 1270 // Existing higher level attention indication trumps ATTENTION_SUCCESS. 1271 if ( 1272 this._attention != DownloadsCommon.ATTENTION_SEVERE && 1273 this._attention != DownloadsCommon.ATTENTION_WARNING 1274 ) { 1275 this.attention = DownloadsCommon.ATTENTION_SUCCESS; 1276 } 1277 } else if (download.error) { 1278 // Existing higher level attention indication trumps ATTENTION_WARNING. 1279 if (this._attention != DownloadsCommon.ATTENTION_SEVERE) { 1280 this.attention = DownloadsCommon.ATTENTION_WARNING; 1281 } 1282 } 1283 }, 1284 1285 onDownloadChanged(download) { 1286 DownloadsViewPrototype.onDownloadChanged.call(this, download); 1287 this._updateViews(); 1288 }, 1289 1290 onDownloadRemoved(download) { 1291 this._itemCount--; 1292 this._updateViews(); 1293 }, 1294 1295 // Propagation of properties to our views 1296 1297 // The following properties are updated by _refreshProperties and are then 1298 // propagated to the views. See _refreshProperties for details. 1299 _hasDownloads: false, 1300 _percentComplete: -1, 1301 1302 /** 1303 * Indicates whether the download indicators should be highlighted. 1304 */ 1305 set attention(aValue) { 1306 this._attention = aValue; 1307 this._updateViews(); 1308 }, 1309 _attention: DownloadsCommon.ATTENTION_NONE, 1310 1311 /** 1312 * Indicates whether the user is interacting with downloads, thus the 1313 * attention indication should not be shown even if requested. 1314 */ 1315 set attentionSuppressed(aValue) { 1316 this._attentionSuppressed = aValue; 1317 this._attention = DownloadsCommon.ATTENTION_NONE; 1318 this._updateViews(); 1319 }, 1320 _attentionSuppressed: false, 1321 1322 /** 1323 * Updates the specified view with the current aggregate values. 1324 * 1325 * @param aView 1326 * DownloadsIndicatorView object to be updated. 1327 */ 1328 _updateView(aView) { 1329 aView.hasDownloads = this._hasDownloads; 1330 aView.percentComplete = this._percentComplete; 1331 aView.attention = this._attentionSuppressed 1332 ? DownloadsCommon.ATTENTION_NONE 1333 : this._attention; 1334 }, 1335 1336 // Property updating based on current download status 1337 1338 /** 1339 * Number of download items that are available to be displayed. 1340 */ 1341 _itemCount: 0, 1342 1343 /** 1344 * A generator function for the Download objects this summary is currently 1345 * interested in. This generator is passed off to summarizeDownloads in order 1346 * to generate statistics about the downloads we care about - in this case, 1347 * it's all active downloads. 1348 */ 1349 *_activeDownloads() { 1350 let downloads = this._isPrivate 1351 ? PrivateDownloadsData.downloads 1352 : DownloadsData.downloads; 1353 for (let download of downloads) { 1354 if (!download.stopped || (download.canceled && download.hasPartialData)) { 1355 yield download; 1356 } 1357 } 1358 }, 1359 1360 /** 1361 * Computes aggregate values based on the current state of downloads. 1362 */ 1363 _refreshProperties() { 1364 let summary = DownloadsCommon.summarizeDownloads(this._activeDownloads()); 1365 1366 // Determine if the indicator should be shown or get attention. 1367 this._hasDownloads = this._itemCount > 0; 1368 1369 // Always show a progress bar if there are downloads in progress. 1370 if (summary.percentComplete >= 0) { 1371 this._percentComplete = summary.percentComplete; 1372 } else if (summary.numDownloading > 0) { 1373 this._percentComplete = 0; 1374 } else { 1375 this._percentComplete = -1; 1376 } 1377 }, 1378}; 1379 1380XPCOMUtils.defineLazyGetter(this, "PrivateDownloadsIndicatorData", function() { 1381 return new DownloadsIndicatorDataCtor(true); 1382}); 1383 1384XPCOMUtils.defineLazyGetter(this, "DownloadsIndicatorData", function() { 1385 return new DownloadsIndicatorDataCtor(false); 1386}); 1387 1388// DownloadsSummaryData 1389 1390/** 1391 * DownloadsSummaryData is a view for DownloadsData that produces a summary 1392 * of all downloads after a certain exclusion point aNumToExclude. For example, 1393 * if there were 5 downloads in progress, and a DownloadsSummaryData was 1394 * constructed with aNumToExclude equal to 3, then that DownloadsSummaryData 1395 * would produce a summary of the last 2 downloads. 1396 * 1397 * @param aIsPrivate 1398 * True if the browser window which owns the download button is a private 1399 * window. 1400 * @param aNumToExclude 1401 * The number of items to exclude from the summary, starting from the 1402 * top of the list. 1403 */ 1404function DownloadsSummaryData(aIsPrivate, aNumToExclude) { 1405 this._numToExclude = aNumToExclude; 1406 // Since we can have multiple instances of DownloadsSummaryData, we 1407 // override these values from the prototype so that each instance can be 1408 // completely separated from one another. 1409 this._loading = false; 1410 1411 this._downloads = []; 1412 1413 // Floating point value indicating the last number of seconds estimated until 1414 // the longest download will finish. We need to store this value so that we 1415 // don't continuously apply smoothing if the actual download state has not 1416 // changed. This is set to -1 if the previous value is unknown. 1417 this._lastRawTimeLeft = -1; 1418 1419 // Last number of seconds estimated until all in-progress downloads with a 1420 // known size and speed will finish. This value is stored to allow smoothing 1421 // in case of small variations. This is set to -1 if the previous value is 1422 // unknown. 1423 this._lastTimeLeft = -1; 1424 1425 // The following properties are updated by _refreshProperties and are then 1426 // propagated to the views. 1427 this._showingProgress = false; 1428 this._details = ""; 1429 this._description = ""; 1430 this._numActive = 0; 1431 this._percentComplete = -1; 1432 1433 this._oldDownloadStates = new WeakMap(); 1434 this._isPrivate = aIsPrivate; 1435 this._views = []; 1436} 1437 1438DownloadsSummaryData.prototype = { 1439 __proto__: DownloadsViewPrototype, 1440 1441 /** 1442 * Removes an object previously added using addView. 1443 * 1444 * @param aView 1445 * DownloadsSummary view to be removed. 1446 */ 1447 removeView(aView) { 1448 DownloadsViewPrototype.removeView.call(this, aView); 1449 1450 if (!this._views.length) { 1451 // Clear out our collection of Download objects. If we ever have 1452 // another view registered with us, this will get re-populated. 1453 this._downloads = []; 1454 } 1455 }, 1456 1457 onDownloadAdded(download) { 1458 DownloadsViewPrototype.onDownloadAdded.call(this, download); 1459 this._downloads.unshift(download); 1460 this._updateViews(); 1461 }, 1462 1463 onDownloadStateChanged() { 1464 // Since the state of a download changed, reset the estimated time left. 1465 this._lastRawTimeLeft = -1; 1466 this._lastTimeLeft = -1; 1467 }, 1468 1469 onDownloadChanged(download) { 1470 DownloadsViewPrototype.onDownloadChanged.call(this, download); 1471 this._updateViews(); 1472 }, 1473 1474 onDownloadRemoved(download) { 1475 let itemIndex = this._downloads.indexOf(download); 1476 this._downloads.splice(itemIndex, 1); 1477 this._updateViews(); 1478 }, 1479 1480 // Propagation of properties to our views 1481 1482 /** 1483 * Updates the specified view with the current aggregate values. 1484 * 1485 * @param aView 1486 * DownloadsIndicatorView object to be updated. 1487 */ 1488 _updateView(aView) { 1489 aView.showingProgress = this._showingProgress; 1490 aView.percentComplete = this._percentComplete; 1491 aView.description = this._description; 1492 aView.details = this._details; 1493 }, 1494 1495 // Property updating based on current download status 1496 1497 /** 1498 * A generator function for the Download objects this summary is currently 1499 * interested in. This generator is passed off to summarizeDownloads in order 1500 * to generate statistics about the downloads we care about - in this case, 1501 * it's the downloads in this._downloads after the first few to exclude, 1502 * which was set when constructing this DownloadsSummaryData instance. 1503 */ 1504 *_downloadsForSummary() { 1505 if (this._downloads.length) { 1506 for (let i = this._numToExclude; i < this._downloads.length; ++i) { 1507 yield this._downloads[i]; 1508 } 1509 } 1510 }, 1511 1512 /** 1513 * Computes aggregate values based on the current state of downloads. 1514 */ 1515 _refreshProperties() { 1516 // Pre-load summary with default values. 1517 let summary = DownloadsCommon.summarizeDownloads( 1518 this._downloadsForSummary() 1519 ); 1520 1521 this._description = DownloadsCommon.strings.otherDownloads3( 1522 summary.numDownloading 1523 ); 1524 this._percentComplete = summary.percentComplete; 1525 1526 // Only show the downloading items. 1527 this._showingProgress = summary.numDownloading > 0; 1528 1529 // Display the estimated time left, if present. 1530 if (summary.rawTimeLeft == -1) { 1531 // There are no downloads with a known time left. 1532 this._lastRawTimeLeft = -1; 1533 this._lastTimeLeft = -1; 1534 this._details = ""; 1535 } else { 1536 // Compute the new time left only if state actually changed. 1537 if (this._lastRawTimeLeft != summary.rawTimeLeft) { 1538 this._lastRawTimeLeft = summary.rawTimeLeft; 1539 this._lastTimeLeft = DownloadsCommon.smoothSeconds( 1540 summary.rawTimeLeft, 1541 this._lastTimeLeft 1542 ); 1543 } 1544 [this._details] = DownloadUtils.getDownloadStatusNoRate( 1545 summary.totalTransferred, 1546 summary.totalSize, 1547 summary.slowestSpeed, 1548 this._lastTimeLeft 1549 ); 1550 } 1551 }, 1552}; 1553