michael@0: /* vim: sw=2 ts=2 sts=2 expandtab filetype=javascript michael@0: * This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: this.EXPORTED_SYMBOLS = [ "DownloadUtils" ]; michael@0: michael@0: /** michael@0: * This module provides the DownloadUtils object which contains useful methods michael@0: * for downloads such as displaying file sizes, transfer times, and download michael@0: * locations. michael@0: * michael@0: * List of methods: michael@0: * michael@0: * [string status, double newLast] michael@0: * getDownloadStatus(int aCurrBytes, [optional] int aMaxBytes, michael@0: * [optional] double aSpeed, [optional] double aLastSec) michael@0: * michael@0: * string progress michael@0: * getTransferTotal(int aCurrBytes, [optional] int aMaxBytes) michael@0: * michael@0: * [string timeLeft, double newLast] michael@0: * getTimeLeft(double aSeconds, [optional] double aLastSec) michael@0: * michael@0: * [string dateCompact, string dateComplete] michael@0: * getReadableDates(Date aDate, [optional] Date aNow) michael@0: * michael@0: * [string displayHost, string fullHost] michael@0: * getURIHost(string aURIString) michael@0: * michael@0: * [string convertedBytes, string units] michael@0: * convertByteUnits(int aBytes) michael@0: * michael@0: * [int time, string units, int subTime, string subUnits] michael@0: * convertTimeUnits(double aSecs) michael@0: */ michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", michael@0: "resource://gre/modules/PluralForm.jsm"); michael@0: michael@0: this.__defineGetter__("gDecimalSymbol", function() { michael@0: delete this.gDecimalSymbol; michael@0: return this.gDecimalSymbol = Number(5.4).toLocaleString().match(/\D/); michael@0: }); michael@0: michael@0: const kDownloadProperties = michael@0: "chrome://mozapps/locale/downloads/downloads.properties"; michael@0: michael@0: let gStr = { michael@0: statusFormat: "statusFormat3", michael@0: statusFormatInfiniteRate: "statusFormatInfiniteRate", michael@0: statusFormatNoRate: "statusFormatNoRate", michael@0: transferSameUnits: "transferSameUnits2", michael@0: transferDiffUnits: "transferDiffUnits2", michael@0: transferNoTotal: "transferNoTotal2", michael@0: timePair: "timePair2", michael@0: timeLeftSingle: "timeLeftSingle2", michael@0: timeLeftDouble: "timeLeftDouble2", michael@0: timeFewSeconds: "timeFewSeconds", michael@0: timeUnknown: "timeUnknown", michael@0: monthDate: "monthDate2", michael@0: yesterday: "yesterday", michael@0: doneScheme: "doneScheme2", michael@0: doneFileScheme: "doneFileScheme", michael@0: units: ["bytes", "kilobyte", "megabyte", "gigabyte"], michael@0: // Update timeSize in convertTimeUnits if changing the length of this array michael@0: timeUnits: ["seconds", "minutes", "hours", "days"], michael@0: infiniteRate: "infiniteRate", michael@0: }; michael@0: michael@0: // This lazily initializes the string bundle upon first use. michael@0: this.__defineGetter__("gBundle", function() { michael@0: delete gBundle; michael@0: return this.gBundle = Cc["@mozilla.org/intl/stringbundle;1"]. michael@0: getService(Ci.nsIStringBundleService). michael@0: createBundle(kDownloadProperties); michael@0: }); michael@0: michael@0: // Keep track of at most this many second/lastSec pairs so that multiple calls michael@0: // to getTimeLeft produce the same time left michael@0: const kCachedLastMaxSize = 10; michael@0: let gCachedLast = []; michael@0: michael@0: this.DownloadUtils = { michael@0: /** michael@0: * Generate a full status string for a download given its current progress, michael@0: * total size, speed, last time remaining michael@0: * michael@0: * @param aCurrBytes michael@0: * Number of bytes transferred so far michael@0: * @param [optional] aMaxBytes michael@0: * Total number of bytes or -1 for unknown michael@0: * @param [optional] aSpeed michael@0: * Current transfer rate in bytes/sec or -1 for unknown michael@0: * @param [optional] aLastSec michael@0: * Last time remaining in seconds or Infinity for unknown michael@0: * @return A pair: [download status text, new value of "last seconds"] michael@0: */ michael@0: getDownloadStatus: function DU_getDownloadStatus(aCurrBytes, aMaxBytes, michael@0: aSpeed, aLastSec) michael@0: { michael@0: let [transfer, timeLeft, newLast, normalizedSpeed] michael@0: = this._deriveTransferRate(aCurrBytes, aMaxBytes, aSpeed, aLastSec); michael@0: michael@0: let [rate, unit] = DownloadUtils.convertByteUnits(normalizedSpeed); michael@0: michael@0: let status; michael@0: if (rate === "Infinity") { michael@0: // Infinity download speed doesn't make sense. Show a localized phrase instead. michael@0: let params = [transfer, gBundle.GetStringFromName(gStr.infiniteRate), timeLeft]; michael@0: status = gBundle.formatStringFromName(gStr.statusFormatInfiniteRate, params, michael@0: params.length); michael@0: } michael@0: else { michael@0: let params = [transfer, rate, unit, timeLeft]; michael@0: status = gBundle.formatStringFromName(gStr.statusFormat, params, michael@0: params.length); michael@0: } michael@0: return [status, newLast]; michael@0: }, michael@0: michael@0: /** michael@0: * Generate a status string for a download given its current progress, michael@0: * total size, speed, last time remaining. The status string contains the michael@0: * time remaining, as well as the total bytes downloaded. Unlike michael@0: * getDownloadStatus, it does not include the rate of download. michael@0: * michael@0: * @param aCurrBytes michael@0: * Number of bytes transferred so far michael@0: * @param [optional] aMaxBytes michael@0: * Total number of bytes or -1 for unknown michael@0: * @param [optional] aSpeed michael@0: * Current transfer rate in bytes/sec or -1 for unknown michael@0: * @param [optional] aLastSec michael@0: * Last time remaining in seconds or Infinity for unknown michael@0: * @return A pair: [download status text, new value of "last seconds"] michael@0: */ michael@0: getDownloadStatusNoRate: michael@0: function DU_getDownloadStatusNoRate(aCurrBytes, aMaxBytes, aSpeed, michael@0: aLastSec) michael@0: { michael@0: let [transfer, timeLeft, newLast] michael@0: = this._deriveTransferRate(aCurrBytes, aMaxBytes, aSpeed, aLastSec); michael@0: michael@0: let params = [transfer, timeLeft]; michael@0: let status = gBundle.formatStringFromName(gStr.statusFormatNoRate, params, michael@0: params.length); michael@0: return [status, newLast]; michael@0: }, michael@0: michael@0: /** michael@0: * Helper function that returns a transfer string, a time remaining string, michael@0: * and a new value of "last seconds". michael@0: * @param aCurrBytes michael@0: * Number of bytes transferred so far michael@0: * @param [optional] aMaxBytes michael@0: * Total number of bytes or -1 for unknown michael@0: * @param [optional] aSpeed michael@0: * Current transfer rate in bytes/sec or -1 for unknown michael@0: * @param [optional] aLastSec michael@0: * Last time remaining in seconds or Infinity for unknown michael@0: * @return A triple: [amount transferred string, time remaining string, michael@0: * new value of "last seconds"] michael@0: */ michael@0: _deriveTransferRate: function DU__deriveTransferRate(aCurrBytes, michael@0: aMaxBytes, aSpeed, michael@0: aLastSec) michael@0: { michael@0: if (aMaxBytes == null) michael@0: aMaxBytes = -1; michael@0: if (aSpeed == null) michael@0: aSpeed = -1; michael@0: if (aLastSec == null) michael@0: aLastSec = Infinity; michael@0: michael@0: // Calculate the time remaining if we have valid values michael@0: let seconds = (aSpeed > 0) && (aMaxBytes > 0) ? michael@0: (aMaxBytes - aCurrBytes) / aSpeed : -1; michael@0: michael@0: let transfer = DownloadUtils.getTransferTotal(aCurrBytes, aMaxBytes); michael@0: let [timeLeft, newLast] = DownloadUtils.getTimeLeft(seconds, aLastSec); michael@0: return [transfer, timeLeft, newLast, aSpeed]; michael@0: }, michael@0: michael@0: /** michael@0: * Generate the transfer progress string to show the current and total byte michael@0: * size. Byte units will be as large as possible and the same units for michael@0: * current and max will be suppressed for the former. michael@0: * michael@0: * @param aCurrBytes michael@0: * Number of bytes transferred so far michael@0: * @param [optional] aMaxBytes michael@0: * Total number of bytes or -1 for unknown michael@0: * @return The transfer progress text michael@0: */ michael@0: getTransferTotal: function DU_getTransferTotal(aCurrBytes, aMaxBytes) michael@0: { michael@0: if (aMaxBytes == null) michael@0: aMaxBytes = -1; michael@0: michael@0: let [progress, progressUnits] = DownloadUtils.convertByteUnits(aCurrBytes); michael@0: let [total, totalUnits] = DownloadUtils.convertByteUnits(aMaxBytes); michael@0: michael@0: // Figure out which byte progress string to display michael@0: let name, values; michael@0: if (aMaxBytes < 0) { michael@0: name = gStr.transferNoTotal; michael@0: values = [ michael@0: progress, michael@0: progressUnits, michael@0: ]; michael@0: } else if (progressUnits == totalUnits) { michael@0: name = gStr.transferSameUnits; michael@0: values = [ michael@0: progress, michael@0: total, michael@0: totalUnits, michael@0: ]; michael@0: } else { michael@0: name = gStr.transferDiffUnits; michael@0: values = [ michael@0: progress, michael@0: progressUnits, michael@0: total, michael@0: totalUnits, michael@0: ]; michael@0: } michael@0: michael@0: return gBundle.formatStringFromName(name, values, values.length); michael@0: }, michael@0: michael@0: /** michael@0: * Generate a "time left" string given an estimate on the time left and the michael@0: * last time. The extra time is used to give a better estimate on the time to michael@0: * show. Both the time values are doubles instead of integers to help get michael@0: * sub-second accuracy for current and future estimates. michael@0: * michael@0: * @param aSeconds michael@0: * Current estimate on number of seconds left for the download michael@0: * @param [optional] aLastSec michael@0: * Last time remaining in seconds or Infinity for unknown michael@0: * @return A pair: [time left text, new value of "last seconds"] michael@0: */ michael@0: getTimeLeft: function DU_getTimeLeft(aSeconds, aLastSec) michael@0: { michael@0: if (aLastSec == null) michael@0: aLastSec = Infinity; michael@0: michael@0: if (aSeconds < 0) michael@0: return [gBundle.GetStringFromName(gStr.timeUnknown), aLastSec]; michael@0: michael@0: // Try to find a cached lastSec for the given second michael@0: aLastSec = gCachedLast.reduce(function(aResult, aItem) michael@0: aItem[0] == aSeconds ? aItem[1] : aResult, aLastSec); michael@0: michael@0: // Add the current second/lastSec pair unless we have too many michael@0: gCachedLast.push([aSeconds, aLastSec]); michael@0: if (gCachedLast.length > kCachedLastMaxSize) michael@0: gCachedLast.shift(); michael@0: michael@0: // Apply smoothing only if the new time isn't a huge change -- e.g., if the michael@0: // new time is more than half the previous time; this is useful for michael@0: // downloads that start/resume slowly michael@0: if (aSeconds > aLastSec / 2) { michael@0: // Apply hysteresis to favor downward over upward swings michael@0: // 30% of down and 10% of up (exponential smoothing) michael@0: let (diff = aSeconds - aLastSec) { michael@0: aSeconds = aLastSec + (diff < 0 ? .3 : .1) * diff; michael@0: } michael@0: michael@0: // If the new time is similar, reuse something close to the last seconds, michael@0: // but subtract a little to provide forward progress michael@0: let diff = aSeconds - aLastSec; michael@0: let diffPct = diff / aLastSec * 100; michael@0: if (Math.abs(diff) < 5 || Math.abs(diffPct) < 5) michael@0: aSeconds = aLastSec - (diff < 0 ? .4 : .2); michael@0: } michael@0: michael@0: // Decide what text to show for the time michael@0: let timeLeft; michael@0: if (aSeconds < 4) { michael@0: // Be friendly in the last few seconds michael@0: timeLeft = gBundle.GetStringFromName(gStr.timeFewSeconds); michael@0: } else { michael@0: // Convert the seconds into its two largest units to display michael@0: let [time1, unit1, time2, unit2] = michael@0: DownloadUtils.convertTimeUnits(aSeconds); michael@0: michael@0: let pair1 = michael@0: gBundle.formatStringFromName(gStr.timePair, [time1, unit1], 2); michael@0: let pair2 = michael@0: gBundle.formatStringFromName(gStr.timePair, [time2, unit2], 2); michael@0: michael@0: // Only show minutes for under 1 hour unless there's a few minutes left; michael@0: // or the second pair is 0. michael@0: if ((aSeconds < 3600 && time1 >= 4) || time2 == 0) { michael@0: timeLeft = gBundle.formatStringFromName(gStr.timeLeftSingle, michael@0: [pair1], 1); michael@0: } else { michael@0: // We've got 2 pairs of times to display michael@0: timeLeft = gBundle.formatStringFromName(gStr.timeLeftDouble, michael@0: [pair1, pair2], 2); michael@0: } michael@0: } michael@0: michael@0: return [timeLeft, aSeconds]; michael@0: }, michael@0: michael@0: /** michael@0: * Converts a Date object to two readable formats, one compact, one complete. michael@0: * The compact format is relative to the current date, and is not an accurate michael@0: * representation. For example, only the time is displayed for today. The michael@0: * complete format always includes both the date and the time, excluding the michael@0: * seconds, and is often shown when hovering the cursor over the compact michael@0: * representation. michael@0: * michael@0: * @param aDate michael@0: * Date object representing the date and time to format. It is assumed michael@0: * that this value represents a past date. michael@0: * @param [optional] aNow michael@0: * Date object representing the current date and time. The real date michael@0: * and time of invocation is used if this parameter is omitted. michael@0: * @return A pair: [compact text, complete text] michael@0: */ michael@0: getReadableDates: function DU_getReadableDates(aDate, aNow) michael@0: { michael@0: if (!aNow) { michael@0: aNow = new Date(); michael@0: } michael@0: michael@0: let dts = Cc["@mozilla.org/intl/scriptabledateformat;1"] michael@0: .getService(Ci.nsIScriptableDateFormat); michael@0: michael@0: // Figure out when today begins michael@0: let today = new Date(aNow.getFullYear(), aNow.getMonth(), aNow.getDate()); michael@0: michael@0: // Figure out if the time is from today, yesterday, this week, etc. michael@0: let dateTimeCompact; michael@0: if (aDate >= today) { michael@0: // After today started, show the time michael@0: dateTimeCompact = dts.FormatTime("", michael@0: dts.timeFormatNoSeconds, michael@0: aDate.getHours(), michael@0: aDate.getMinutes(), michael@0: 0); michael@0: } else if (today - aDate < (24 * 60 * 60 * 1000)) { michael@0: // After yesterday started, show yesterday michael@0: dateTimeCompact = gBundle.GetStringFromName(gStr.yesterday); michael@0: } else if (today - aDate < (6 * 24 * 60 * 60 * 1000)) { michael@0: // After last week started, show day of week michael@0: dateTimeCompact = aDate.toLocaleFormat("%A"); michael@0: } else { michael@0: // Show month/day michael@0: let month = aDate.toLocaleFormat("%B"); michael@0: // Remove leading 0 by converting the date string to a number michael@0: let date = Number(aDate.toLocaleFormat("%d")); michael@0: dateTimeCompact = gBundle.formatStringFromName(gStr.monthDate, [month, date], 2); michael@0: } michael@0: michael@0: let dateTimeFull = dts.FormatDateTime("", michael@0: dts.dateFormatLong, michael@0: dts.timeFormatNoSeconds, michael@0: aDate.getFullYear(), michael@0: aDate.getMonth() + 1, michael@0: aDate.getDate(), michael@0: aDate.getHours(), michael@0: aDate.getMinutes(), michael@0: 0); michael@0: michael@0: return [dateTimeCompact, dateTimeFull]; michael@0: }, michael@0: michael@0: /** michael@0: * Get the appropriate display host string for a URI string depending on if michael@0: * the URI has an eTLD + 1, is an IP address, a local file, or other protocol michael@0: * michael@0: * @param aURIString michael@0: * The URI string to try getting an eTLD + 1, etc. michael@0: * @return A pair: [display host for the URI string, full host name] michael@0: */ michael@0: getURIHost: function DU_getURIHost(aURIString) michael@0: { michael@0: let ioService = Cc["@mozilla.org/network/io-service;1"]. michael@0: getService(Ci.nsIIOService); michael@0: let eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"]. michael@0: getService(Ci.nsIEffectiveTLDService); michael@0: let idnService = Cc["@mozilla.org/network/idn-service;1"]. michael@0: getService(Ci.nsIIDNService); michael@0: michael@0: // Get a URI that knows about its components michael@0: let uri = ioService.newURI(aURIString, null, null); michael@0: michael@0: // Get the inner-most uri for schemes like jar: michael@0: if (uri instanceof Ci.nsINestedURI) michael@0: uri = uri.innermostURI; michael@0: michael@0: let fullHost; michael@0: try { michael@0: // Get the full host name; some special URIs fail (data: jar:) michael@0: fullHost = uri.host; michael@0: } catch (e) { michael@0: fullHost = ""; michael@0: } michael@0: michael@0: let displayHost; michael@0: try { michael@0: // This might fail if it's an IP address or doesn't have more than 1 part michael@0: let baseDomain = eTLDService.getBaseDomain(uri); michael@0: michael@0: // Convert base domain for display; ignore the isAscii out param michael@0: displayHost = idnService.convertToDisplayIDN(baseDomain, {}); michael@0: } catch (e) { michael@0: // Default to the host name michael@0: displayHost = fullHost; michael@0: } michael@0: michael@0: // Check if we need to show something else for the host michael@0: if (uri.scheme == "file") { michael@0: // Display special text for file protocol michael@0: displayHost = gBundle.GetStringFromName(gStr.doneFileScheme); michael@0: fullHost = displayHost; michael@0: } else if (displayHost.length == 0) { michael@0: // Got nothing; show the scheme (data: about: moz-icon:) michael@0: displayHost = michael@0: gBundle.formatStringFromName(gStr.doneScheme, [uri.scheme], 1); michael@0: fullHost = displayHost; michael@0: } else if (uri.port != -1) { michael@0: // Tack on the port if it's not the default port michael@0: let port = ":" + uri.port; michael@0: displayHost += port; michael@0: fullHost += port; michael@0: } michael@0: michael@0: return [displayHost, fullHost]; michael@0: }, michael@0: michael@0: /** michael@0: * Converts a number of bytes to the appropriate unit that results in an michael@0: * internationalized number that needs fewer than 4 digits. michael@0: * michael@0: * @param aBytes michael@0: * Number of bytes to convert michael@0: * @return A pair: [new value with 3 sig. figs., its unit] michael@0: */ michael@0: convertByteUnits: function DU_convertByteUnits(aBytes) michael@0: { michael@0: let unitIndex = 0; michael@0: michael@0: // Convert to next unit if it needs 4 digits (after rounding), but only if michael@0: // we know the name of the next unit michael@0: while ((aBytes >= 999.5) && (unitIndex < gStr.units.length - 1)) { michael@0: aBytes /= 1024; michael@0: unitIndex++; michael@0: } michael@0: michael@0: // Get rid of insignificant bits by truncating to 1 or 0 decimal points michael@0: // 0 -> 0; 1.2 -> 1.2; 12.3 -> 12.3; 123.4 -> 123; 234.5 -> 235 michael@0: // added in bug 462064: (unitIndex != 0) makes sure that no decimal digit for bytes appears when aBytes < 100 michael@0: aBytes = aBytes.toFixed((aBytes > 0) && (aBytes < 100) && (unitIndex != 0) ? 1 : 0); michael@0: michael@0: if (gDecimalSymbol != ".") michael@0: aBytes = aBytes.replace(".", gDecimalSymbol); michael@0: return [aBytes, gBundle.GetStringFromName(gStr.units[unitIndex])]; michael@0: }, michael@0: michael@0: /** michael@0: * Converts a number of seconds to the two largest units. Time values are michael@0: * whole numbers, and units have the correct plural/singular form. michael@0: * michael@0: * @param aSecs michael@0: * Seconds to convert into the appropriate 2 units michael@0: * @return 4-item array [first value, its unit, second value, its unit] michael@0: */ michael@0: convertTimeUnits: function DU_convertTimeUnits(aSecs) michael@0: { michael@0: // These are the maximum values for seconds, minutes, hours corresponding michael@0: // with gStr.timeUnits without the last item michael@0: let timeSize = [60, 60, 24]; michael@0: michael@0: let time = aSecs; michael@0: let scale = 1; michael@0: let unitIndex = 0; michael@0: michael@0: // Keep converting to the next unit while we have units left and the michael@0: // current one isn't the largest unit possible michael@0: while ((unitIndex < timeSize.length) && (time >= timeSize[unitIndex])) { michael@0: time /= timeSize[unitIndex]; michael@0: scale *= timeSize[unitIndex]; michael@0: unitIndex++; michael@0: } michael@0: michael@0: let value = convertTimeUnitsValue(time); michael@0: let units = convertTimeUnitsUnits(value, unitIndex); michael@0: michael@0: let extra = aSecs - value * scale; michael@0: let nextIndex = unitIndex - 1; michael@0: michael@0: // Convert the extra time to the next largest unit michael@0: for (let index = 0; index < nextIndex; index++) michael@0: extra /= timeSize[index]; michael@0: michael@0: let value2 = convertTimeUnitsValue(extra); michael@0: let units2 = convertTimeUnitsUnits(value2, nextIndex); michael@0: michael@0: return [value, units, value2, units2]; michael@0: }, michael@0: }; michael@0: michael@0: /** michael@0: * Private helper for convertTimeUnits that gets the display value of a time michael@0: * michael@0: * @param aTime michael@0: * Time value for display michael@0: * @return An integer value for the time rounded down michael@0: */ michael@0: function convertTimeUnitsValue(aTime) michael@0: { michael@0: return Math.floor(aTime); michael@0: } michael@0: michael@0: /** michael@0: * Private helper for convertTimeUnits that gets the display units of a time michael@0: * michael@0: * @param aTime michael@0: * Time value for display michael@0: * @param aIndex michael@0: * Index into gStr.timeUnits for the appropriate unit michael@0: * @return The appropriate plural form of the unit for the time michael@0: */ michael@0: function convertTimeUnitsUnits(aTime, aIndex) michael@0: { michael@0: // Negative index would be an invalid unit, so just give empty michael@0: if (aIndex < 0) michael@0: return ""; michael@0: michael@0: return PluralForm.get(aTime, gBundle.GetStringFromName(gStr.timeUnits[aIndex])); michael@0: } michael@0: michael@0: /** michael@0: * Private helper function to log errors to the error console and command line michael@0: * michael@0: * @param aMsg michael@0: * Error message to log or an array of strings to concat michael@0: */ michael@0: function log(aMsg) michael@0: { michael@0: let msg = "DownloadUtils.jsm: " + (aMsg.join ? aMsg.join("") : aMsg); michael@0: Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService). michael@0: logStringMessage(msg); michael@0: dump(msg + "\n"); michael@0: }