CLOSED TREE: TraceMonkey merge head. (a=blockers)
[mozilla-central.git] / toolkit / mozapps / downloads / DownloadUtils.jsm
blob52c0f5860229f81bf81d2b83bbbd95fe985fdbf0
1 /* ***** BEGIN LICENSE BLOCK *****
2  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
3  *
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/
8  *
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
12  * License.
13  *
14  * The Original Code is Download Manager Utility Code.
15  *
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.
20  *
21  * Contributor(s):
22  *
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.
34  *
35  * ***** END LICENSE BLOCK ***** */
37 var EXPORTED_SYMBOLS = [ "DownloadUtils" ];
39 /**
40  * This module provides the DownloadUtils object which contains useful methods
41  * for downloads such as displaying file sizes, transfer times, and download
42  * locations.
43  *
44  * List of methods:
45  *
46  * [string status, double newLast]
47  * getDownloadStatus(int aCurrBytes, [optional] int aMaxBytes,
48  *                   [optional] double aSpeed, [optional] double aLastSec)
49  *
50  * string progress
51  * getTransferTotal(int aCurrBytes, [optional] int aMaxBytes)
52  *
53  * [string timeLeft, double newLast]
54  * getTimeLeft(double aSeconds, [optional] double aLastSec)
55  *
56  * [string displayHost, string fullHost]
57  * getURIHost(string aURIString)
58  *
59  * [string convertedBytes, string units]
60  * convertByteUnits(int aBytes)
61  *
62  * [int time, string units, int subTime, string subUnits]
63  * convertTimeUnits(double aSecs)
64  */
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");
73   return PluralForm;
74 });
76 __defineGetter__("gDecimalSymbol", function() {
77   delete this.gDecimalSymbol;
78   return this.gDecimalSymbol = Number(5.4).toLocaleString().match(/\D/);
79 });
81 const kDownloadProperties =
82   "chrome://mozapps/locale/downloads/downloads.properties";
84 // These strings will be converted to the corresponding ones from the string
85 // bundle on use
86 let kStrings = {
87   statusFormat: "statusFormat2",
88   transferSameUnits: "transferSameUnits",
89   transferDiffUnits: "transferDiffUnits",
90   transferNoTotal: "transferNoTotal",
91   timePair: "timePair",
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
104 let gStr = {
105   /**
106    * Initialize lazy string getters
107    */
108   _init: function()
109   {
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));
115   },
117   /**
118    * Convert strings to those in the string bundle. This lazily loads the
119    * string bundle *once* only when used the first time.
120    */
121   get _getStr()
122   {
123     // Delete the getter to be overwritten
124     delete gStr._getStr;
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).
130                  GetStringFromName;
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
135       delete gStr[name];
137       try {
138         // "name" is a string or array of the stringbundle-loaded "value"
139         return gStr[name] = typeof value == "string" ?
140                             getStr(value) :
141                             value.map(getStr);
142       } catch (e) {
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
146       }
147     };
148   },
150 // Initialize the lazy string getters!
151 gStr._init();
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 = {
159   /**
160    * Generate a full status string for a download given its current progress,
161    * total size, speed, last time remaining
162    *
163    * @param aCurrBytes
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"]
172    */
173   getDownloadStatus: function DU_getDownloadStatus(aCurrBytes, aMaxBytes,
174                                                    aSpeed, aLastSec)
175   {
176     if (aMaxBytes == null)
177       aMaxBytes = -1;
178     if (aSpeed == null)
179       aSpeed = -1;
180     if (aLastSec == null)
181       aLastSec = Infinity;
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
188     let status;
189     let (transfer = DownloadUtils.getTransferTotal(aCurrBytes, aMaxBytes)) {
190       // Insert 1 is the download progress
191       status = replaceInsert(gStr.statusFormat, 1, transfer);
192     }
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);
200     }
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];
208     }
209   },
211   /**
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.
215    *
216    * @param aCurrBytes
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
221    */
222   getTransferTotal: function DU_getTransferTotal(aCurrBytes, aMaxBytes)
223   {
224     if (aMaxBytes == null)
225       aMaxBytes = -1;
227     let [progress, progressUnits] = DownloadUtils.convertByteUnits(aCurrBytes);
228     let [total, totalUnits] = DownloadUtils.convertByteUnits(aMaxBytes);
230     // Figure out which byte progress string to display
231     let transfer;
232     if (aMaxBytes < 0)
233       transfer = gStr.transferNoTotal;
234     else if (progressUnits == totalUnits)
235       transfer = gStr.transferSameUnits;
236     else
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);
244     return transfer;
245   },
247   /**
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.
252    *
253    * @param aSeconds
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"]
258    */
259   getTimeLeft: function DU_getTimeLeft(aSeconds, aLastSec)
260   {
261     if (aLastSec == null)
262       aLastSec = Infinity;
264     if (aSeconds < 0)
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)
274       gCachedLast.shift();
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;
284       }
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);
292     }
294     // Decide what text to show for the time
295     let timeLeft;
296     if (aSeconds < 4) {
297       // Be friendly in the last few seconds
298       timeLeft = gStr.timeFewSeconds;
299     } else {
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);
313       } else {
314         // We've got 2 pairs of times to display
315         timeLeft = replaceInsert(gStr.timeLeftDouble, 1, pair1);
316         timeLeft = replaceInsert(timeLeft, 2, pair2);
317       }
318     }
320     return [timeLeft, aSeconds];
321   },
323   /**
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
326    *
327    * @param aURIString
328    *        The URI string to try getting an eTLD + 1, etc.
329    * @return A pair: [display host for the URI string, full host name]
330    */
331   getURIHost: function DU_getURIHost(aURIString)
332   {
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;
347     let fullHost;
348     try {
349       // Get the full host name; some special URIs fail (data: jar:)
350       fullHost = uri.host;
351     } catch (e) {
352       fullHost = "";
353     }
355     let displayHost;
356     try {
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, {});
362     } catch (e) {
363       // Default to the host name
364       displayHost = fullHost;
365     }
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;
379       displayHost += port;
380       fullHost += port;
381     }
383     return [displayHost, fullHost];
384   },
386   /**
387    * Converts a number of bytes to the appropriate unit that results in an
388    * internationalized number that needs fewer than 4 digits.
389    *
390    * @param aBytes
391    *        Number of bytes to convert
392    * @return A pair: [new value with 3 sig. figs., its unit]
393    */
394   convertByteUnits: function DU_convertByteUnits(aBytes)
395   {
396     let unitIndex = 0;
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)) {
401       aBytes /= 1024;
402       unitIndex++;
403     }
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]];
413   },
415   /**
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.
418    *
419    * @param aSecs
420    *        Seconds to convert into the appropriate 2 units
421    * @return 4-item array [first value, its unit, second value, its unit]
422    */
423   convertTimeUnits: function DU_convertTimeUnits(aSecs)
424   {
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];
429     let time = aSecs;
430     let scale = 1;
431     let unitIndex = 0;
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];
438       unitIndex++;
439     }
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];
455   },
459  * Private helper for convertTimeUnits that gets the display value of a time
461  * @param aTime
462  *        Time value for display
463  * @return An integer value for the time rounded down
464  */
465 function convertTimeUnitsValue(aTime)
467   return Math.floor(aTime);
471  * Private helper for convertTimeUnits that gets the display units of a time
473  * @param aTime
474  *        Time value for display
475  * @param aIndex
476  *        Index into gStr.timeUnits for the appropriate unit
477  * @return The appropriate plural form of the unit for the time
478  */
479 function convertTimeUnitsUnits(aTime, aIndex)
481   // Negative index would be an invalid unit, so just give empty
482   if (aIndex < 0)
483     return "";
485   return PluralForm.get(aTime, gStr.timeUnits[aIndex]);
489  * Private helper function to replace a placeholder string with a real string
491  * @param aText
492  *        Source text containing placeholder (e.g., #1)
493  * @param aIndex
494  *        Index number of placeholder to replace
495  * @param aValue
496  *        New string to put in place of placeholder
497  * @return The string with placeholder replaced with the new string
498  */
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
507  * @param aMsg
508  *        Error message to log or an array of strings to concat
509  */
510 function log(aMsg)
512   let msg = "DownloadUtils.jsm: " + (aMsg.join ? aMsg.join("") : aMsg);
513   Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).
514     logStringMessage(msg);
515   dump(msg + "\n");