/* ***** BEGIN LICENSE BLOCK ***** * Version: MPL 1.1/GPL 2.0/LGPL 2.1 * * The contents of this file are subject to the Mozilla Public License Version * 1.1 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * http://www.mozilla.org/MPL/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * * The Original Code is Download Manager Utility Code. * * The Initial Developer of the Original Code is * Edward Lee . * Portions created by the Initial Developer are Copyright (C) 2008 * the Initial Developer. All Rights Reserved. * * Contributor(s): * * Alternatively, the contents of this file may be used under the terms of * either the GNU General Public License Version 2 or later (the "GPL"), or * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), * in which case the provisions of the GPL or the LGPL are applicable instead * of those above. If you wish to allow use of your version of this file only * under the terms of either the GPL or the LGPL, and not to allow others to * use your version of this file under the terms of the MPL, indicate your * decision by deleting the provisions above and replace them with the notice * and other provisions required by the GPL or the LGPL. If you do not delete * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ var EXPORTED_SYMBOLS = [ "DownloadUtils" ]; /** * This module provides the DownloadUtils object which contains useful methods * for downloads such as displaying file sizes, transfer times, and download * locations. * * List of methods: * * [string status, double newLast] * getDownloadStatus(int aCurrBytes, [optional] int aMaxBytes, * [optional] double aSpeed, [optional] double aLastSec) * * string progress * getTransferTotal(int aCurrBytes, [optional] int aMaxBytes) * * [string timeLeft, double newLast] * getTimeLeft(double aSeconds, [optional] double aLastSec) * * [string displayHost, string fullHost] * getURIHost(string aURIString) * * [double convertedBytes, string units] * convertByteUnits(int aBytes) * * [int time, string units, int subTime, string subUnits] * convertTimeUnits(double aSecs) */ const Cc = Components.classes; const Ci = Components.interfaces; const Cu = Components.utils; __defineGetter__("PluralForm", function() { delete this.PluralForm; Cu.import("resource://gre/modules/PluralForm.jsm"); return PluralForm; }); const kDownloadProperties = "chrome://mozapps/locale/downloads/downloads.properties"; // These strings will be converted to the corresponding ones from the string // bundle on use let kStrings = { statusFormat: "statusFormat2", transferSameUnits: "transferSameUnits", transferDiffUnits: "transferDiffUnits", transferNoTotal: "transferNoTotal", timePair: "timePair", timeLeftSingle: "timeLeftSingle", timeLeftDouble: "timeLeftDouble", timeFewSeconds: "timeFewSeconds", timeUnknown: "timeUnknown", doneScheme: "doneScheme", doneFileScheme: "doneFileScheme", units: ["bytes", "kilobyte", "megabyte", "gigabyte"], // Update timeSize in convertTimeUnits if changing the length of this array timeUnits: ["seconds", "minutes", "hours", "days"], }; // This object will lazily load the strings defined in kStrings let gStr = { /** * Initialize lazy string getters */ _init: function() { // Make each "name" a lazy-loading string that knows how to load itself. We // need to locally scope name and value to keep them around for the getter. for (let [name, value] in Iterator(kStrings)) let ([n, v] = [name, value]) gStr.__defineGetter__(n, function() gStr._getStr(n, v)); }, /** * Convert strings to those in the string bundle. This lazily loads the * string bundle *once* only when used the first time. */ get _getStr() { // Delete the getter to be overwritten delete gStr._getStr; // Lazily load the bundle into the closure on first call to _getStr let getStr = Cc["@mozilla.org/intl/stringbundle;1"]. getService(Ci.nsIStringBundleService). createBundle(kDownloadProperties). GetStringFromName; // _getStr is a function that sets string "name" to stringbundle's "value" return gStr._getStr = function(name, value) { // Delete the getter to be overwritten delete gStr[name]; try { // "name" is a string or array of the stringbundle-loaded "value" return gStr[name] = typeof value == "string" ? getStr(value) : value.map(getStr); } catch (e) { log(["Couldn't get string '", name, "' from property '", value, "'"]); // Don't return anything (undefined), and because we deleted ourselves, // future accesses will also be undefined } }; }, }; // Initialize the lazy string getters! gStr._init(); // Keep track of at most this many second/lastSec pairs so that multiple calls // to getTimeLeft produce the same time left const kCachedLastMaxSize = 10; let gCachedLast = []; let DownloadUtils = { /** * Generate a full status string for a download given its current progress, * total size, speed, last time remaining * * @param aCurrBytes * Number of bytes transferred so far * @param [optional] aMaxBytes * Total number of bytes or -1 for unknown * @param [optional] aSpeed * Current transfer rate in bytes/sec or -1 for unknown * @param [optional] aLastSec * Last time remaining in seconds or Infinity for unknown * @return A pair: [download status text, new value of "last seconds"] */ getDownloadStatus: function DU_getDownloadStatus(aCurrBytes, aMaxBytes, aSpeed, aLastSec) { if (aMaxBytes == null) aMaxBytes = -1; if (aSpeed == null) aSpeed = -1; if (aLastSec == null) aLastSec = Infinity; // Calculate the time remaining if we have valid values let seconds = (aSpeed > 0) && (aMaxBytes > 0) ? (aMaxBytes - aCurrBytes) / aSpeed : -1; // Update the bytes transferred and bytes total let status; let (transfer = DownloadUtils.getTransferTotal(aCurrBytes, aMaxBytes)) { // Insert 1 is the download progress status = replaceInsert(gStr.statusFormat, 1, transfer); } // Update the download rate let ([rate, unit] = DownloadUtils.convertByteUnits(aSpeed)) { // Insert 2 is the download rate status = replaceInsert(status, 2, rate); // Insert 3 is the |unit|/sec status = replaceInsert(status, 3, unit); } // Update time remaining let ([timeLeft, newLast] = DownloadUtils.getTimeLeft(seconds, aLastSec)) { // Insert 4 is the time remaining status = replaceInsert(status, 4, timeLeft); return [status, newLast]; } }, /** * Generate the transfer progress string to show the current and total byte * size. Byte units will be as large as possible and the same units for * current and max will be supressed for the former. * * @param aCurrBytes * Number of bytes transferred so far * @param [optional] aMaxBytes * Total number of bytes or -1 for unknown * @return The transfer progress text */ getTransferTotal: function DU_getTransferTotal(aCurrBytes, aMaxBytes) { if (aMaxBytes == null) aMaxBytes = -1; let [progress, progressUnits] = DownloadUtils.convertByteUnits(aCurrBytes); let [total, totalUnits] = DownloadUtils.convertByteUnits(aMaxBytes); // Figure out which byte progress string to display let transfer; if (total < 0) transfer = gStr.transferNoTotal; else if (progressUnits == totalUnits) transfer = gStr.transferSameUnits; else transfer = gStr.transferDiffUnits; transfer = replaceInsert(transfer, 1, progress); transfer = replaceInsert(transfer, 2, progressUnits); transfer = replaceInsert(transfer, 3, total); transfer = replaceInsert(transfer, 4, totalUnits); return transfer; }, /** * Generate a "time left" string given an estimate on the time left and the * last time. The extra time is used to give a better estimate on the time to * show. Both the time values are doubles instead of integers to help get * sub-second accuracy for current and future estimates. * * @param aSeconds * Current estimate on number of seconds left for the download * @param [optional] aLastSec * Last time remaining in seconds or Infinity for unknown * @return A pair: [time left text, new value of "last seconds"] */ getTimeLeft: function DU_getTimeLeft(aSeconds, aLastSec) { if (aLastSec == null) aLastSec = Infinity; if (aSeconds < 0) return [gStr.timeUnknown, aLastSec]; // Try to find a cached lastSec for the given second aLastSec = gCachedLast.reduce(function(aResult, aItem) aItem[0] == aSeconds ? aItem[1] : aResult, aLastSec); // Add the current second/lastSec pair unless we have too many gCachedLast.push([aSeconds, aLastSec]); if (gCachedLast.length > kCachedLastMaxSize) gCachedLast.shift(); // Apply smoothing only if the new time isn't a huge change -- e.g., if the // new time is more than half the previous time; this is useful for // downloads that start/resume slowly if (aSeconds > aLastSec / 2) { // Apply hysteresis to favor downward over upward swings // 30% of down and 10% of up (exponential smoothing) let (diff = aSeconds - aLastSec) { aSeconds = aLastSec + (diff < 0 ? .3 : .1) * diff; } // If the new time is similar, reuse something close to the last seconds, // but subtract a little to provide forward progress let diff = aSeconds - aLastSec; let diffPct = diff / aLastSec * 100; if (Math.abs(diff) < 5 || Math.abs(diffPct) < 5) aSeconds = aLastSec - (diff < 0 ? .4 : .2); } // Decide what text to show for the time let timeLeft; if (aSeconds < 4) { // Be friendly in the last few seconds timeLeft = gStr.timeFewSeconds; } else { // Convert the seconds into its two largest units to display let [time1, unit1, time2, unit2] = DownloadUtils.convertTimeUnits(aSeconds); let pair1 = replaceInsert(gStr.timePair, 1, time1); pair1 = replaceInsert(pair1, 2, unit1); let pair2 = replaceInsert(gStr.timePair, 1, time2); pair2 = replaceInsert(pair2, 2, unit2); // Only show minutes for under 1 hour unless there's a few minutes left; // or the second pair is 0. if ((aSeconds < 3600 && time1 >= 4) || time2 == 0) { timeLeft = replaceInsert(gStr.timeLeftSingle, 1, pair1); } else { // We've got 2 pairs of times to display timeLeft = replaceInsert(gStr.timeLeftDouble, 1, pair1); timeLeft = replaceInsert(timeLeft, 2, pair2); } } return [timeLeft, aSeconds]; }, /** * Get the appropriate display host string for a URI string depending on if * the URI has an eTLD + 1, is an IP address, a local file, or other protocol * * @param aURIString * The URI string to try getting an eTLD + 1, etc. * @return A pair: [display host for the URI string, full host name] */ getURIHost: function DU_getURIHost(aURIString) { let ioService = Cc["@mozilla.org/network/io-service;1"]. getService(Ci.nsIIOService); let eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"]. getService(Ci.nsIEffectiveTLDService); let idnService = Cc["@mozilla.org/network/idn-service;1"]. getService(Ci.nsIIDNService); // Get a URI that knows about its components let uri = ioService.newURI(aURIString, null, null); // Get the inner-most uri for schemes like jar: if (uri instanceof Ci.nsINestedURI) uri = uri.innermostURI; let fullHost; try { // Get the full host name; some special URIs fail (data: jar:) fullHost = uri.host; } catch (e) { fullHost = ""; } let displayHost; try { // This might fail if it's an IP address or doesn't have more than 1 part let baseDomain = eTLDService.getBaseDomain(uri); // Convert base domain for display; ignore the isAscii out param displayHost = idnService.convertToDisplayIDN(baseDomain, {}); } catch (e) { // Default to the host name displayHost = fullHost; } // Check if we need to show something else for the host if (uri.scheme == "file") { // Display special text for file protocol displayHost = gStr.doneFileScheme; fullHost = displayHost; } else if (displayHost.length == 0) { // Got nothing; show the scheme (data: about: moz-icon:) displayHost = replaceInsert(gStr.doneScheme, 1, uri.scheme); fullHost = displayHost; } else if (uri.port != -1) { // Tack on the port if it's not the default port let port = ":" + uri.port; displayHost += port; fullHost += port; } return [displayHost, fullHost]; }, /** * Converts a number of bytes to the appropriate unit that results in a * number that needs fewer than 4 digits * * @param aBytes * Number of bytes to convert * @return A pair: [new value with 3 sig. figs., its unit] */ convertByteUnits: function DU_convertByteUnits(aBytes) { let unitIndex = 0; // Convert to next unit if it needs 4 digits (after rounding), but only if // we know the name of the next unit while ((aBytes >= 999.5) && (unitIndex < gStr.units.length - 1)) { aBytes /= 1024; unitIndex++; } // Get rid of insignificant bits by truncating to 1 or 0 decimal points // 0 -> 0; 1.2 -> 1.2; 12.3 -> 12.3; 123.4 -> 123; 234.5 -> 235 aBytes = aBytes.toFixed((aBytes > 0) && (aBytes < 100) ? 1 : 0); return [aBytes, gStr.units[unitIndex]]; }, /** * Converts a number of seconds to the two largest units. Time values are * whole numbers, and units have the correct plural/singular form. * * @param aSecs * Seconds to convert into the appropriate 2 units * @return 4-item array [first value, its unit, second value, its unit] */ convertTimeUnits: function DU_convertTimeUnits(aSecs) { // These are the maximum values for seconds, minutes, hours corresponding // with gStr.timeUnits without the last item let timeSize = [60, 60, 24]; let time = aSecs; let scale = 1; let unitIndex = 0; // Keep converting to the next unit while we have units left and the // current one isn't the largest unit possible while ((unitIndex < timeSize.length) && (time >= timeSize[unitIndex])) { time /= timeSize[unitIndex]; scale *= timeSize[unitIndex]; unitIndex++; } let value = convertTimeUnitsValue(time); let units = convertTimeUnitsUnits(value, unitIndex); let extra = aSecs - value * scale; let nextIndex = unitIndex - 1; // Convert the extra time to the next largest unit for (let index = 0; index < nextIndex; index++) extra /= timeSize[index]; let value2 = convertTimeUnitsValue(extra); let units2 = convertTimeUnitsUnits(value2, nextIndex); return [value, units, value2, units2]; }, }; /** * Private helper for convertTimeUnits that gets the display value of a time * * @param aTime * Time value for display * @return An integer value for the time rounded down */ function convertTimeUnitsValue(aTime) { return Math.floor(aTime); } /** * Private helper for convertTimeUnits that gets the display units of a time * * @param aTime * Time value for display * @param aIndex * Index into gStr.timeUnits for the appropriate unit * @return The appropriate plural form of the unit for the time */ function convertTimeUnitsUnits(aTime, aIndex) { // Negative index would be an invalid unit, so just give empty if (aIndex < 0) return ""; return PluralForm.get(aTime, gStr.timeUnits[aIndex]); } /** * Private helper function to replace a placeholder string with a real string * * @param aText * Source text containing placeholder (e.g., #1) * @param aIndex * Index number of placeholder to replace * @param aValue * New string to put in place of placeholder * @return The string with placeholder replaced with the new string */ function replaceInsert(aText, aIndex, aValue) { return aText.replace("#" + aIndex, aValue); } /** * Private helper function to log errors to the error console and command line * * @param aMsg * Error message to log or an array of strings to concat */ function log(aMsg) { let msg = "DownloadUtils.jsm: " + (aMsg.join ? aMsg.join("") : aMsg); Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService). logStringMessage(msg); dump(msg + "\n"); }