1 /* ***** BEGIN LICENSE BLOCK *****
2 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
4 * The contents of this file are subject to the Mozilla Public License Version
5 * 1.1 (the "License"); you may not use this file except in compliance with
6 * the License. You may obtain a copy of the License at
7 * http://www.mozilla.org/MPL/
9 * Software distributed under the License is distributed on an "AS IS" basis,
10 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
11 * for the specific language governing rights and limitations under the
14 * The Original Code is Download Manager Utility Code.
16 * The Initial Developer of the Original Code is
17 * Edward Lee <edward.lee@engineering.uiuc.edu>.
18 * Portions created by the Initial Developer are Copyright (C) 2008
19 * the Initial Developer. All Rights Reserved.
23 * Alternatively, the contents of this file may be used under the terms of
24 * either the GNU General Public License Version 2 or later (the "GPL"), or
25 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
26 * in which case the provisions of the GPL or the LGPL are applicable instead
27 * of those above. If you wish to allow use of your version of this file only
28 * under the terms of either the GPL or the LGPL, and not to allow others to
29 * use your version of this file under the terms of the MPL, indicate your
30 * decision by deleting the provisions above and replace them with the notice
31 * and other provisions required by the GPL or the LGPL. If you do not delete
32 * the provisions above, a recipient may use your version of this file under
33 * the terms of any one of the MPL, the GPL or the LGPL.
35 * ***** END LICENSE BLOCK ***** */
37 var EXPORTED_SYMBOLS = [ "DownloadUtils" ];
40 * This module provides the DownloadUtils object which contains useful methods
41 * for downloads such as displaying file sizes, transfer times, and download
46 * [string status, double newLast]
47 * getDownloadStatus(int aCurrBytes, [optional] int aMaxBytes,
48 * [optional] double aSpeed, [optional] double aLastSec)
51 * getTransferTotal(int aCurrBytes, [optional] int aMaxBytes)
53 * [string timeLeft, double newLast]
54 * getTimeLeft(double aSeconds, [optional] double aLastSec)
56 * [string displayHost, string fullHost]
57 * getURIHost(string aURIString)
59 * [string convertedBytes, string units]
60 * convertByteUnits(int aBytes)
62 * [int time, string units, int subTime, string subUnits]
63 * convertTimeUnits(double aSecs)
66 const Cc = Components.classes;
67 const Ci = Components.interfaces;
68 const Cu = Components.utils;
70 __defineGetter__("PluralForm", function() {
71 delete this.PluralForm;
72 Cu.import("resource://gre/modules/PluralForm.jsm");
76 __defineGetter__("gDecimalSymbol", function() {
77 delete this.gDecimalSymbol;
78 return this.gDecimalSymbol = Number(5.4).toLocaleString().match(/\D/);
81 const kDownloadProperties =
82 "chrome://mozapps/locale/downloads/downloads.properties";
84 // These strings will be converted to the corresponding ones from the string
87 statusFormat: "statusFormat2",
88 transferSameUnits: "transferSameUnits",
89 transferDiffUnits: "transferDiffUnits",
90 transferNoTotal: "transferNoTotal",
92 timeLeftSingle: "timeLeftSingle",
93 timeLeftDouble: "timeLeftDouble",
94 timeFewSeconds: "timeFewSeconds",
95 timeUnknown: "timeUnknown",
96 doneScheme: "doneScheme",
97 doneFileScheme: "doneFileScheme",
98 units: ["bytes", "kilobyte", "megabyte", "gigabyte"],
99 // Update timeSize in convertTimeUnits if changing the length of this array
100 timeUnits: ["seconds", "minutes", "hours", "days"],
103 // This object will lazily load the strings defined in kStrings
106 * Initialize lazy string getters
110 // Make each "name" a lazy-loading string that knows how to load itself. We
111 // need to locally scope name and value to keep them around for the getter.
112 for (let [name, value] in Iterator(kStrings))
113 let ([n, v] = [name, value])
114 gStr.__defineGetter__(n, function() gStr._getStr(n, v));
118 * Convert strings to those in the string bundle. This lazily loads the
119 * string bundle *once* only when used the first time.
123 // Delete the getter to be overwritten
126 // Lazily load the bundle into the closure on first call to _getStr
127 let getStr = Cc["@mozilla.org/intl/stringbundle;1"].
128 getService(Ci.nsIStringBundleService).
129 createBundle(kDownloadProperties).
132 // _getStr is a function that sets string "name" to stringbundle's "value"
133 return gStr._getStr = function(name, value) {
134 // Delete the getter to be overwritten
138 // "name" is a string or array of the stringbundle-loaded "value"
139 return gStr[name] = typeof value == "string" ?
143 log(["Couldn't get string '", name, "' from property '", value, "'"]);
144 // Don't return anything (undefined), and because we deleted ourselves,
145 // future accesses will also be undefined
150 // Initialize the lazy string getters!
153 // Keep track of at most this many second/lastSec pairs so that multiple calls
154 // to getTimeLeft produce the same time left
155 const kCachedLastMaxSize = 10;
156 let gCachedLast = [];
158 let DownloadUtils = {
160 * Generate a full status string for a download given its current progress,
161 * total size, speed, last time remaining
164 * Number of bytes transferred so far
165 * @param [optional] aMaxBytes
166 * Total number of bytes or -1 for unknown
167 * @param [optional] aSpeed
168 * Current transfer rate in bytes/sec or -1 for unknown
169 * @param [optional] aLastSec
170 * Last time remaining in seconds or Infinity for unknown
171 * @return A pair: [download status text, new value of "last seconds"]
173 getDownloadStatus: function DU_getDownloadStatus(aCurrBytes, aMaxBytes,
176 if (aMaxBytes == null)
180 if (aLastSec == null)
183 // Calculate the time remaining if we have valid values
184 let seconds = (aSpeed > 0) && (aMaxBytes > 0) ?
185 (aMaxBytes - aCurrBytes) / aSpeed : -1;
187 // Update the bytes transferred and bytes total
189 let (transfer = DownloadUtils.getTransferTotal(aCurrBytes, aMaxBytes)) {
190 // Insert 1 is the download progress
191 status = replaceInsert(gStr.statusFormat, 1, transfer);
194 // Update the download rate
195 let ([rate, unit] = DownloadUtils.convertByteUnits(aSpeed)) {
196 // Insert 2 is the download rate
197 status = replaceInsert(status, 2, rate);
198 // Insert 3 is the |unit|/sec
199 status = replaceInsert(status, 3, unit);
202 // Update time remaining
203 let ([timeLeft, newLast] = DownloadUtils.getTimeLeft(seconds, aLastSec)) {
204 // Insert 4 is the time remaining
205 status = replaceInsert(status, 4, timeLeft);
207 return [status, newLast];
212 * Generate the transfer progress string to show the current and total byte
213 * size. Byte units will be as large as possible and the same units for
214 * current and max will be suppressed for the former.
217 * Number of bytes transferred so far
218 * @param [optional] aMaxBytes
219 * Total number of bytes or -1 for unknown
220 * @return The transfer progress text
222 getTransferTotal: function DU_getTransferTotal(aCurrBytes, aMaxBytes)
224 if (aMaxBytes == null)
227 let [progress, progressUnits] = DownloadUtils.convertByteUnits(aCurrBytes);
228 let [total, totalUnits] = DownloadUtils.convertByteUnits(aMaxBytes);
230 // Figure out which byte progress string to display
233 transfer = gStr.transferNoTotal;
234 else if (progressUnits == totalUnits)
235 transfer = gStr.transferSameUnits;
237 transfer = gStr.transferDiffUnits;
239 transfer = replaceInsert(transfer, 1, progress);
240 transfer = replaceInsert(transfer, 2, progressUnits);
241 transfer = replaceInsert(transfer, 3, total);
242 transfer = replaceInsert(transfer, 4, totalUnits);
248 * Generate a "time left" string given an estimate on the time left and the
249 * last time. The extra time is used to give a better estimate on the time to
250 * show. Both the time values are doubles instead of integers to help get
251 * sub-second accuracy for current and future estimates.
254 * Current estimate on number of seconds left for the download
255 * @param [optional] aLastSec
256 * Last time remaining in seconds or Infinity for unknown
257 * @return A pair: [time left text, new value of "last seconds"]
259 getTimeLeft: function DU_getTimeLeft(aSeconds, aLastSec)
261 if (aLastSec == null)
265 return [gStr.timeUnknown, aLastSec];
267 // Try to find a cached lastSec for the given second
268 aLastSec = gCachedLast.reduce(function(aResult, aItem)
269 aItem[0] == aSeconds ? aItem[1] : aResult, aLastSec);
271 // Add the current second/lastSec pair unless we have too many
272 gCachedLast.push([aSeconds, aLastSec]);
273 if (gCachedLast.length > kCachedLastMaxSize)
276 // Apply smoothing only if the new time isn't a huge change -- e.g., if the
277 // new time is more than half the previous time; this is useful for
278 // downloads that start/resume slowly
279 if (aSeconds > aLastSec / 2) {
280 // Apply hysteresis to favor downward over upward swings
281 // 30% of down and 10% of up (exponential smoothing)
282 let (diff = aSeconds - aLastSec) {
283 aSeconds = aLastSec + (diff < 0 ? .3 : .1) * diff;
286 // If the new time is similar, reuse something close to the last seconds,
287 // but subtract a little to provide forward progress
288 let diff = aSeconds - aLastSec;
289 let diffPct = diff / aLastSec * 100;
290 if (Math.abs(diff) < 5 || Math.abs(diffPct) < 5)
291 aSeconds = aLastSec - (diff < 0 ? .4 : .2);
294 // Decide what text to show for the time
297 // Be friendly in the last few seconds
298 timeLeft = gStr.timeFewSeconds;
300 // Convert the seconds into its two largest units to display
301 let [time1, unit1, time2, unit2] =
302 DownloadUtils.convertTimeUnits(aSeconds);
304 let pair1 = replaceInsert(gStr.timePair, 1, time1);
305 pair1 = replaceInsert(pair1, 2, unit1);
306 let pair2 = replaceInsert(gStr.timePair, 1, time2);
307 pair2 = replaceInsert(pair2, 2, unit2);
309 // Only show minutes for under 1 hour unless there's a few minutes left;
310 // or the second pair is 0.
311 if ((aSeconds < 3600 && time1 >= 4) || time2 == 0) {
312 timeLeft = replaceInsert(gStr.timeLeftSingle, 1, pair1);
314 // We've got 2 pairs of times to display
315 timeLeft = replaceInsert(gStr.timeLeftDouble, 1, pair1);
316 timeLeft = replaceInsert(timeLeft, 2, pair2);
320 return [timeLeft, aSeconds];
324 * Get the appropriate display host string for a URI string depending on if
325 * the URI has an eTLD + 1, is an IP address, a local file, or other protocol
328 * The URI string to try getting an eTLD + 1, etc.
329 * @return A pair: [display host for the URI string, full host name]
331 getURIHost: function DU_getURIHost(aURIString)
333 let ioService = Cc["@mozilla.org/network/io-service;1"].
334 getService(Ci.nsIIOService);
335 let eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"].
336 getService(Ci.nsIEffectiveTLDService);
337 let idnService = Cc["@mozilla.org/network/idn-service;1"].
338 getService(Ci.nsIIDNService);
340 // Get a URI that knows about its components
341 let uri = ioService.newURI(aURIString, null, null);
343 // Get the inner-most uri for schemes like jar:
344 if (uri instanceof Ci.nsINestedURI)
345 uri = uri.innermostURI;
349 // Get the full host name; some special URIs fail (data: jar:)
357 // This might fail if it's an IP address or doesn't have more than 1 part
358 let baseDomain = eTLDService.getBaseDomain(uri);
360 // Convert base domain for display; ignore the isAscii out param
361 displayHost = idnService.convertToDisplayIDN(baseDomain, {});
363 // Default to the host name
364 displayHost = fullHost;
367 // Check if we need to show something else for the host
368 if (uri.scheme == "file") {
369 // Display special text for file protocol
370 displayHost = gStr.doneFileScheme;
371 fullHost = displayHost;
372 } else if (displayHost.length == 0) {
373 // Got nothing; show the scheme (data: about: moz-icon:)
374 displayHost = replaceInsert(gStr.doneScheme, 1, uri.scheme);
375 fullHost = displayHost;
376 } else if (uri.port != -1) {
377 // Tack on the port if it's not the default port
378 let port = ":" + uri.port;
383 return [displayHost, fullHost];
387 * Converts a number of bytes to the appropriate unit that results in an
388 * internationalized number that needs fewer than 4 digits.
391 * Number of bytes to convert
392 * @return A pair: [new value with 3 sig. figs., its unit]
394 convertByteUnits: function DU_convertByteUnits(aBytes)
398 // Convert to next unit if it needs 4 digits (after rounding), but only if
399 // we know the name of the next unit
400 while ((aBytes >= 999.5) && (unitIndex < gStr.units.length - 1)) {
405 // Get rid of insignificant bits by truncating to 1 or 0 decimal points
406 // 0 -> 0; 1.2 -> 1.2; 12.3 -> 12.3; 123.4 -> 123; 234.5 -> 235
407 // added in bug 462064: (unitIndex != 0) makes sure that no decimal digit for bytes appears when aBytes < 100
408 aBytes = aBytes.toFixed((aBytes > 0) && (aBytes < 100) && (unitIndex != 0) ? 1 : 0);
410 if (gDecimalSymbol != ".")
411 aBytes = aBytes.replace(".", gDecimalSymbol);
412 return [aBytes, gStr.units[unitIndex]];
416 * Converts a number of seconds to the two largest units. Time values are
417 * whole numbers, and units have the correct plural/singular form.
420 * Seconds to convert into the appropriate 2 units
421 * @return 4-item array [first value, its unit, second value, its unit]
423 convertTimeUnits: function DU_convertTimeUnits(aSecs)
425 // These are the maximum values for seconds, minutes, hours corresponding
426 // with gStr.timeUnits without the last item
427 let timeSize = [60, 60, 24];
433 // Keep converting to the next unit while we have units left and the
434 // current one isn't the largest unit possible
435 while ((unitIndex < timeSize.length) && (time >= timeSize[unitIndex])) {
436 time /= timeSize[unitIndex];
437 scale *= timeSize[unitIndex];
441 let value = convertTimeUnitsValue(time);
442 let units = convertTimeUnitsUnits(value, unitIndex);
444 let extra = aSecs - value * scale;
445 let nextIndex = unitIndex - 1;
447 // Convert the extra time to the next largest unit
448 for (let index = 0; index < nextIndex; index++)
449 extra /= timeSize[index];
451 let value2 = convertTimeUnitsValue(extra);
452 let units2 = convertTimeUnitsUnits(value2, nextIndex);
454 return [value, units, value2, units2];
459 * Private helper for convertTimeUnits that gets the display value of a time
462 * Time value for display
463 * @return An integer value for the time rounded down
465 function convertTimeUnitsValue(aTime)
467 return Math.floor(aTime);
471 * Private helper for convertTimeUnits that gets the display units of a time
474 * Time value for display
476 * Index into gStr.timeUnits for the appropriate unit
477 * @return The appropriate plural form of the unit for the time
479 function convertTimeUnitsUnits(aTime, aIndex)
481 // Negative index would be an invalid unit, so just give empty
485 return PluralForm.get(aTime, gStr.timeUnits[aIndex]);
489 * Private helper function to replace a placeholder string with a real string
492 * Source text containing placeholder (e.g., #1)
494 * Index number of placeholder to replace
496 * New string to put in place of placeholder
497 * @return The string with placeholder replaced with the new string
499 function replaceInsert(aText, aIndex, aValue)
501 return aText.replace("#" + aIndex, aValue);
505 * Private helper function to log errors to the error console and command line
508 * Error message to log or an array of strings to concat
512 let msg = "DownloadUtils.jsm: " + (aMsg.join ? aMsg.join("") : aMsg);
513 Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).
514 logStringMessage(msg);