!import
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 ***** */
36
37 var EXPORTED_SYMBOLS = [ "DownloadUtils" ];
38
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 * [double convertedBytes, string units]
60 * convertByteUnits(int aBytes)
61 *
62 * [int time, string units, int subTime, string subUnits]
63 * convertTimeUnits(double aSecs)
64 */
65
66 const Cc = Components.classes;
67 const Ci = Components.interfaces;
68 const Cu = Components.utils
69 Cu.import("resource://gre/modules/PluralForm.jsm");
70
71 const kDownloadProperties =
72 "chrome://mozapps/locale/downloads/downloads.properties";
73
74 // These strings will be converted to the corresponding ones from the string
75 // bundle on use
76 let kStrings = {
77 statusFormat: "statusFormat2",
78 transferSameUnits: "transferSameUnits",
79 transferDiffUnits: "transferDiffUnits",
80 transferNoTotal: "transferNoTotal",
81 timePair: "timePair",
82 timeLeftSingle: "timeLeftSingle",
83 timeLeftDouble: "timeLeftDouble",
84 timeFewSeconds: "timeFewSeconds",
85 timeUnknown: "timeUnknown",
86 doneScheme: "doneScheme",
87 doneFileScheme: "doneFileScheme",
88 units: ["bytes", "kilobyte", "megabyte", "gigabyte"],
89 // Update timeSize in convertTimeUnits if changing the length of this array
90 timeUnits: ["seconds", "minutes", "hours", "days"],
91 };
92
93 // This object will lazily load the strings defined in kStrings
94 let gStr = {
95 /**
96 * Initialize lazy string getters
97 */
_init
98 _init: function()
99 {
100 // Make each "name" a lazy-loading string that knows how to load itself. We
101 // need to locally scope name and value to keep them around for the getter.
102 for (let [name, value] in Iterator(kStrings))
103 let ([n, v] = [name, value])
anon:104:33
104 gStr.__defineGetter__(n, function() gStr._getStr(n, v));
105 },
106
107 /**
108 * Convert strings to those in the string bundle. This lazily loads the
109 * string bundle *once* only when used the first time.
110 */
get__getStr
111 get _getStr()
112 {
113 // Delete the getter to be overwritten
114 delete gStr._getStr;
115
116 // Lazily load the bundle into the closure on first call to _getStr
117 let getStr = Cc["@mozilla.org/intl/stringbundle;1"].
118 getService(Ci.nsIStringBundleService).
119 createBundle(kDownloadProperties).
120 GetStringFromName;
121
122 // _getStr is a function that sets string "name" to stringbundle's "value"
anon:123:26
123 return gStr._getStr = function(name, value) {
124 // Delete the getter to be overwritten
125 delete gStr[name];
126
127 try {
128 // "name" is a string or array of the stringbundle-loaded "value"
129 return gStr[name] = typeof value == "string" ?
130 getStr(value) :
131 value.map(getStr);
132 } catch (e) {
133 log(["Couldn't get string '", name, "' from property '", value, "'"]);
134 // Don't return anything (undefined), and because we deleted ourselves,
135 // future accesses will also be undefined
136 }
137 };
138 },
139 };
140 // Initialize the lazy string getters!
141 gStr._init();
142
143 // Keep track of at most this many second/lastSec pairs so that multiple calls
144 // to getTimeLeft produce the same time left
145 const kCachedLastMaxSize = 10;
146 let gCachedLast = [];
147
148 let DownloadUtils = {
149 /**
150 * Generate a full status string for a download given its current progress,
151 * total size, speed, last time remaining
152 *
153 * @param aCurrBytes
154 * Number of bytes transferred so far
155 * @param [optional] aMaxBytes
156 * Total number of bytes or -1 for unknown
157 * @param [optional] aSpeed
158 * Current transfer rate in bytes/sec or -1 for unknown
159 * @param [optional] aLastSec
160 * Last time remaining in seconds or Infinity for unknown
161 * @return A pair: [download status text, new value of "last seconds"]
162 */
getDownloadStatus
163 getDownloadStatus: function(aCurrBytes, aMaxBytes, aSpeed, aLastSec)
164 {
165 if (isNil(aMaxBytes))
166 aMaxBytes = -1;
167 if (isNil(aSpeed))
168 aSpeed = -1;
169 if (isNil(aLastSec))
170 aLastSec = Infinity;
171
172 // Calculate the time remaining if we have valid values
173 let seconds = (aSpeed > 0) && (aMaxBytes > 0) ?
174 (aMaxBytes - aCurrBytes) / aSpeed : -1;
175
176 // Update the bytes transferred and bytes total
177 let status;
178 let (transfer = DownloadUtils.getTransferTotal(aCurrBytes, aMaxBytes)) {
179 // Insert 1 is the download progress
180 status = replaceInsert(gStr.statusFormat, 1, transfer);
181 }
182
183 // Update the download rate
184 let ([rate, unit] = DownloadUtils.convertByteUnits(aSpeed)) {
185 // Insert 2 is the download rate
186 status = replaceInsert(status, 2, rate);
187 // Insert 3 is the |unit|/sec
188 status = replaceInsert(status, 3, unit);
189 }
190
191 // Update time remaining
192 let ([timeLeft, newLast] = DownloadUtils.getTimeLeft(seconds, aLastSec)) {
193 // Insert 4 is the time remaining
194 status = replaceInsert(status, 4, timeLeft);
195
196 return [status, newLast];
197 }
198 },
199
200 /**
201 * Generate the transfer progress string to show the current and total byte
202 * size. Byte units will be as large as possible and the same units for
203 * current and max will be supressed for the former.
204 *
205 * @param aCurrBytes
206 * Number of bytes transferred so far
207 * @param [optional] aMaxBytes
208 * Total number of bytes or -1 for unknown
209 * @return The transfer progress text
210 */
getTransferTotal
211 getTransferTotal: function(aCurrBytes, aMaxBytes)
212 {
213 if (isNil(aMaxBytes))
214 aMaxBytes = -1;
215
216 let [progress, progressUnits] = DownloadUtils.convertByteUnits(aCurrBytes);
217 let [total, totalUnits] = DownloadUtils.convertByteUnits(aMaxBytes);
218
219 // Figure out which byte progress string to display
220 let transfer;
221 if (total < 0)
222 transfer = gStr.transferNoTotal;
223 else if (progressUnits == totalUnits)
224 transfer = gStr.transferSameUnits;
225 else
226 transfer = gStr.transferDiffUnits;
227
228 transfer = replaceInsert(transfer, 1, progress);
229 transfer = replaceInsert(transfer, 2, progressUnits);
230 transfer = replaceInsert(transfer, 3, total);
231 transfer = replaceInsert(transfer, 4, totalUnits);
232
233 return transfer;
234 },
235
236 /**
237 * Generate a "time left" string given an estimate on the time left and the
238 * last time. The extra time is used to give a better estimate on the time to
239 * show. Both the time values are doubles instead of integers to help get
240 * sub-second accuracy for current and future estimates.
241 *
242 * @param aSeconds
243 * Current estimate on number of seconds left for the download
244 * @param [optional] aLastSec
245 * Last time remaining in seconds or Infinity for unknown
246 * @return A pair: [time left text, new value of "last seconds"]
247 */
getTimeLeft
248 getTimeLeft: function(aSeconds, aLastSec)
249 {
250 if (isNil(aLastSec))
251 aLastSec = Infinity;
252
253 if (aSeconds < 0)
254 return [gStr.timeUnknown, aLastSec];
255
256 // Try to find a cached lastSec for the given second
anon:257:34
257 aLastSec = gCachedLast.reduce(function(aResult, aItem)
258 aItem[0] == aSeconds ? aItem[1] : aResult, aLastSec);
259
260 // Add the current second/lastSec pair unless we have too many
261 gCachedLast.push([aSeconds, aLastSec]);
262 if (gCachedLast.length > kCachedLastMaxSize)
263 gCachedLast.shift();
264
265 // Apply smoothing only if the new time isn't a huge change -- e.g., if the
266 // new time is more than half the previous time; this is useful for
267 // downloads that start/resume slowly
268 if (aSeconds > aLastSec / 2) {
269 // Apply hysteresis to favor downward over upward swings
270 // 30% of down and 10% of up (exponential smoothing)
271 let (diff = aSeconds - aLastSec) {
272 aSeconds = aLastSec + (diff < 0 ? .3 : .1) * diff;
273 }
274
275 // If the new time is similar, reuse something close to the last seconds,
276 // but subtract a little to provide forward progress
277 let diff = aSeconds - aLastSec;
278 let diffPct = diff / aLastSec * 100;
279 if (Math.abs(diff) < 5 || Math.abs(diffPct) < 5)
280 aSeconds = aLastSec - (diff < 0 ? .4 : .2);
281 }
282
283 // Decide what text to show for the time
284 let timeLeft;
285 if (aSeconds < 4) {
286 // Be friendly in the last few seconds
287 timeLeft = gStr.timeFewSeconds;
288 } else {
289 // Convert the seconds into its two largest units to display
290 let [time1, unit1, time2, unit2] =
291 DownloadUtils.convertTimeUnits(aSeconds);
292
293 let pair1 = replaceInsert(gStr.timePair, 1, time1);
294 pair1 = replaceInsert(pair1, 2, unit1);
295 let pair2 = replaceInsert(gStr.timePair, 1, time2);
296 pair2 = replaceInsert(pair2, 2, unit2);
297
298 // Only show minutes for under 1 hour or the second pair is 0
299 if (aSeconds < 3600 || time2 == 0) {
300 timeLeft = replaceInsert(gStr.timeLeftSingle, 1, pair1);
301 } else {
302 // We've got 2 pairs of times to display
303 timeLeft = replaceInsert(gStr.timeLeftDouble, 1, pair1);
304 timeLeft = replaceInsert(timeLeft, 2, pair2);
305 }
306 }
307
308 return [timeLeft, aSeconds];
309 },
310
311 /**
312 * Get the appropriate display host string for a URI string depending on if
313 * the URI has an eTLD + 1, is an IP address, a local file, or other protocol
314 *
315 * @param aURIString
316 * The URI string to try getting an eTLD + 1, etc.
317 * @return A pair: [display host for the URI string, full host name]
318 */
getURIHost
319 getURIHost: function(aURIString)
320 {
321 let ioService = Cc["@mozilla.org/network/io-service;1"].
322 getService(Ci.nsIIOService);
323 let eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"].
324 getService(Ci.nsIEffectiveTLDService);
325 let idnService = Cc["@mozilla.org/network/idn-service;1"].
326 getService(Ci.nsIIDNService);
327
328 // Get a URI that knows about its components
329 let uri = ioService.newURI(aURIString, null, null);
330
331 // Get the inner-most uri for schemes like jar:
332 if (uri instanceof Ci.nsINestedURI)
333 uri = uri.innermostURI;
334
335 let fullHost;
336 try {
337 // Get the full host name; some special URIs fail (data: jar:)
338 fullHost = uri.host;
339 } catch (e) {
340 fullHost = "";
341 }
342
343 let displayHost;
344 try {
345 // This might fail if it's an IP address or doesn't have more than 1 part
346 let baseDomain = eTLDService.getBaseDomain(uri);
347
348 // Convert base domain for display; ignore the isAscii out param
349 displayHost = idnService.convertToDisplayIDN(baseDomain, {});
350 } catch (e) {
351 // Default to the host name
352 displayHost = fullHost;
353 }
354
355 // Check if we need to show something else for the host
356 if (uri.scheme == "file") {
357 // Display special text for file protocol
358 displayHost = gStr.doneFileScheme;
359 fullHost = displayHost;
360 } else if (displayHost.length == 0) {
361 // Got nothing; show the scheme (data: about: moz-icon:)
362 displayHost = replaceInsert(gStr.doneScheme, 1, uri.scheme);
363 fullHost = displayHost;
364 } else if (uri.port != -1) {
365 // Tack on the port if it's not the default port
366 let port = ":" + uri.port;
367 displayHost += port;
368 fullHost += port;
369 }
370
371 return [displayHost, fullHost];
372 },
373
374 /**
375 * Converts a number of bytes to the appropriate unit that results in a
376 * number that needs fewer than 4 digits
377 *
378 * @param aBytes
379 * Number of bytes to convert
380 * @return A pair: [new value with 3 sig. figs., its unit]
381 */
convertByteUnits
382 convertByteUnits: function(aBytes)
383 {
384 let unitIndex = 0;
385
386 // Convert to next unit if it needs 4 digits (after rounding), but only if
387 // we know the name of the next unit
388 while ((aBytes >= 999.5) && (unitIndex < gStr.units.length - 1)) {
389 aBytes /= 1024;
390 unitIndex++;
391 }
392
393 // Get rid of insignificant bits by truncating to 1 or 0 decimal points
394 // 0 -> 0; 1.2 -> 1.2; 12.3 -> 12.3; 123.4 -> 123; 234.5 -> 235
395 aBytes = aBytes.toFixed((aBytes > 0) && (aBytes < 100) ? 1 : 0);
396
397 return [aBytes, gStr.units[unitIndex]];
398 },
399
400 /**
401 * Converts a number of seconds to the two largest units. Time values are
402 * whole numbers, and units have the correct plural/singular form.
403 *
404 * @param aSecs
405 * Seconds to convert into the appropriate 2 units
406 * @return 4-item array [first value, its unit, second value, its unit]
407 */
convertTimeUnits
408 convertTimeUnits: function(aSecs)
409 {
410 // These are the maximum values for seconds, minutes, hours corresponding
411 // with gStr.timeUnits without the last item
412 let timeSize = [60, 60, 24];
413
414 let time = aSecs;
415 let scale = 1;
416 let unitIndex = 0;
417
418 // Keep converting to the next unit while we have units left and the
419 // current one isn't the largest unit possible
420 while ((unitIndex < timeSize.length) && (time >= timeSize[unitIndex])) {
421 time /= timeSize[unitIndex];
422 scale *= timeSize[unitIndex];
423 unitIndex++;
424 }
425
426 let value = convertTimeUnitsValue(time);
427 let units = convertTimeUnitsUnits(value, unitIndex);
428
429 let extra = aSecs - value * scale;
430 let nextIndex = unitIndex - 1;
431
432 // Convert the extra time to the next largest unit
433 for (let index = 0; index < nextIndex; index++)
434 extra /= timeSize[index];
435
436 let value2 = convertTimeUnitsValue(extra);
437 let units2 = convertTimeUnitsUnits(value2, nextIndex);
438
439 return [value, units, value2, units2];
440 },
441 };
442
443 /**
444 * Private helper for convertTimeUnits that gets the display value of a time
445 *
446 * @param aTime
447 * Time value for display
448 * @return An integer value for the time rounded down
449 */
convertTimeUnitsValue
450 function convertTimeUnitsValue(aTime)
451 {
452 return Math.floor(aTime);
453 }
454
455 /**
456 * Private helper for convertTimeUnits that gets the display units of a time
457 *
458 * @param aTime
459 * Time value for display
460 * @param aIndex
461 * Index into gStr.timeUnits for the appropriate unit
462 * @return The appropriate plural form of the unit for the time
463 */
convertTimeUnitsUnits
464 function convertTimeUnitsUnits(aTime, aIndex)
465 {
466 // Negative index would be an invalid unit, so just give empty
467 if (aIndex < 0)
468 return "";
469
470 return PluralForm.get(aTime, gStr.timeUnits[aIndex]);
471 }
472
473 /**
474 * Private helper function to replace a placeholder string with a real string
475 *
476 * @param aText
477 * Source text containing placeholder (e.g., #1)
478 * @param aIndex
479 * Index number of placeholder to replace
480 * @param aValue
481 * New string to put in place of placeholder
482 * @return The string with placeholder replaced with the new string
483 */
replaceInsert
484 function replaceInsert(aText, aIndex, aValue)
485 {
486 return aText.replace("#" + aIndex, aValue);
487 }
488
489 /**
490 * Private helper function to determine if an argument is null or undefined
491 *
492 * @param aArg
493 * The argument to check for nullness or undefinedness
494 * @return true if null or undefined, false otherwise
495 */
isNil
496 function isNil(aArg)
497 {
498 return (aArg == null) || (aArg == undefined);
499 }
500
501 /**
502 * Private helper function to log errors to the error console and command line
503 *
504 * @param aMsg
505 * Error message to log or an array of strings to concat
506 */
log
507 function log(aMsg)
508 {
509 let msg = "DownloadUtils.jsm: " + (aMsg.join ? aMsg.join("") : aMsg);
510 Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).
511 logStringMessage(msg);
512 dump(msg + "\n");
513 }