|
1 /* vim: sw=2 ts=2 sts=2 expandtab filetype=javascript |
|
2 * This Source Code Form is subject to the terms of the Mozilla Public |
|
3 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
5 |
|
6 this.EXPORTED_SYMBOLS = [ "DownloadUtils" ]; |
|
7 |
|
8 /** |
|
9 * This module provides the DownloadUtils object which contains useful methods |
|
10 * for downloads such as displaying file sizes, transfer times, and download |
|
11 * locations. |
|
12 * |
|
13 * List of methods: |
|
14 * |
|
15 * [string status, double newLast] |
|
16 * getDownloadStatus(int aCurrBytes, [optional] int aMaxBytes, |
|
17 * [optional] double aSpeed, [optional] double aLastSec) |
|
18 * |
|
19 * string progress |
|
20 * getTransferTotal(int aCurrBytes, [optional] int aMaxBytes) |
|
21 * |
|
22 * [string timeLeft, double newLast] |
|
23 * getTimeLeft(double aSeconds, [optional] double aLastSec) |
|
24 * |
|
25 * [string dateCompact, string dateComplete] |
|
26 * getReadableDates(Date aDate, [optional] Date aNow) |
|
27 * |
|
28 * [string displayHost, string fullHost] |
|
29 * getURIHost(string aURIString) |
|
30 * |
|
31 * [string convertedBytes, string units] |
|
32 * convertByteUnits(int aBytes) |
|
33 * |
|
34 * [int time, string units, int subTime, string subUnits] |
|
35 * convertTimeUnits(double aSecs) |
|
36 */ |
|
37 |
|
38 const Cc = Components.classes; |
|
39 const Ci = Components.interfaces; |
|
40 const Cu = Components.utils; |
|
41 |
|
42 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
43 |
|
44 XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", |
|
45 "resource://gre/modules/PluralForm.jsm"); |
|
46 |
|
47 this.__defineGetter__("gDecimalSymbol", function() { |
|
48 delete this.gDecimalSymbol; |
|
49 return this.gDecimalSymbol = Number(5.4).toLocaleString().match(/\D/); |
|
50 }); |
|
51 |
|
52 const kDownloadProperties = |
|
53 "chrome://mozapps/locale/downloads/downloads.properties"; |
|
54 |
|
55 let gStr = { |
|
56 statusFormat: "statusFormat3", |
|
57 statusFormatInfiniteRate: "statusFormatInfiniteRate", |
|
58 statusFormatNoRate: "statusFormatNoRate", |
|
59 transferSameUnits: "transferSameUnits2", |
|
60 transferDiffUnits: "transferDiffUnits2", |
|
61 transferNoTotal: "transferNoTotal2", |
|
62 timePair: "timePair2", |
|
63 timeLeftSingle: "timeLeftSingle2", |
|
64 timeLeftDouble: "timeLeftDouble2", |
|
65 timeFewSeconds: "timeFewSeconds", |
|
66 timeUnknown: "timeUnknown", |
|
67 monthDate: "monthDate2", |
|
68 yesterday: "yesterday", |
|
69 doneScheme: "doneScheme2", |
|
70 doneFileScheme: "doneFileScheme", |
|
71 units: ["bytes", "kilobyte", "megabyte", "gigabyte"], |
|
72 // Update timeSize in convertTimeUnits if changing the length of this array |
|
73 timeUnits: ["seconds", "minutes", "hours", "days"], |
|
74 infiniteRate: "infiniteRate", |
|
75 }; |
|
76 |
|
77 // This lazily initializes the string bundle upon first use. |
|
78 this.__defineGetter__("gBundle", function() { |
|
79 delete gBundle; |
|
80 return this.gBundle = Cc["@mozilla.org/intl/stringbundle;1"]. |
|
81 getService(Ci.nsIStringBundleService). |
|
82 createBundle(kDownloadProperties); |
|
83 }); |
|
84 |
|
85 // Keep track of at most this many second/lastSec pairs so that multiple calls |
|
86 // to getTimeLeft produce the same time left |
|
87 const kCachedLastMaxSize = 10; |
|
88 let gCachedLast = []; |
|
89 |
|
90 this.DownloadUtils = { |
|
91 /** |
|
92 * Generate a full status string for a download given its current progress, |
|
93 * total size, speed, last time remaining |
|
94 * |
|
95 * @param aCurrBytes |
|
96 * Number of bytes transferred so far |
|
97 * @param [optional] aMaxBytes |
|
98 * Total number of bytes or -1 for unknown |
|
99 * @param [optional] aSpeed |
|
100 * Current transfer rate in bytes/sec or -1 for unknown |
|
101 * @param [optional] aLastSec |
|
102 * Last time remaining in seconds or Infinity for unknown |
|
103 * @return A pair: [download status text, new value of "last seconds"] |
|
104 */ |
|
105 getDownloadStatus: function DU_getDownloadStatus(aCurrBytes, aMaxBytes, |
|
106 aSpeed, aLastSec) |
|
107 { |
|
108 let [transfer, timeLeft, newLast, normalizedSpeed] |
|
109 = this._deriveTransferRate(aCurrBytes, aMaxBytes, aSpeed, aLastSec); |
|
110 |
|
111 let [rate, unit] = DownloadUtils.convertByteUnits(normalizedSpeed); |
|
112 |
|
113 let status; |
|
114 if (rate === "Infinity") { |
|
115 // Infinity download speed doesn't make sense. Show a localized phrase instead. |
|
116 let params = [transfer, gBundle.GetStringFromName(gStr.infiniteRate), timeLeft]; |
|
117 status = gBundle.formatStringFromName(gStr.statusFormatInfiniteRate, params, |
|
118 params.length); |
|
119 } |
|
120 else { |
|
121 let params = [transfer, rate, unit, timeLeft]; |
|
122 status = gBundle.formatStringFromName(gStr.statusFormat, params, |
|
123 params.length); |
|
124 } |
|
125 return [status, newLast]; |
|
126 }, |
|
127 |
|
128 /** |
|
129 * Generate a status string for a download given its current progress, |
|
130 * total size, speed, last time remaining. The status string contains the |
|
131 * time remaining, as well as the total bytes downloaded. Unlike |
|
132 * getDownloadStatus, it does not include the rate of download. |
|
133 * |
|
134 * @param aCurrBytes |
|
135 * Number of bytes transferred so far |
|
136 * @param [optional] aMaxBytes |
|
137 * Total number of bytes or -1 for unknown |
|
138 * @param [optional] aSpeed |
|
139 * Current transfer rate in bytes/sec or -1 for unknown |
|
140 * @param [optional] aLastSec |
|
141 * Last time remaining in seconds or Infinity for unknown |
|
142 * @return A pair: [download status text, new value of "last seconds"] |
|
143 */ |
|
144 getDownloadStatusNoRate: |
|
145 function DU_getDownloadStatusNoRate(aCurrBytes, aMaxBytes, aSpeed, |
|
146 aLastSec) |
|
147 { |
|
148 let [transfer, timeLeft, newLast] |
|
149 = this._deriveTransferRate(aCurrBytes, aMaxBytes, aSpeed, aLastSec); |
|
150 |
|
151 let params = [transfer, timeLeft]; |
|
152 let status = gBundle.formatStringFromName(gStr.statusFormatNoRate, params, |
|
153 params.length); |
|
154 return [status, newLast]; |
|
155 }, |
|
156 |
|
157 /** |
|
158 * Helper function that returns a transfer string, a time remaining string, |
|
159 * and a new value of "last seconds". |
|
160 * @param aCurrBytes |
|
161 * Number of bytes transferred so far |
|
162 * @param [optional] aMaxBytes |
|
163 * Total number of bytes or -1 for unknown |
|
164 * @param [optional] aSpeed |
|
165 * Current transfer rate in bytes/sec or -1 for unknown |
|
166 * @param [optional] aLastSec |
|
167 * Last time remaining in seconds or Infinity for unknown |
|
168 * @return A triple: [amount transferred string, time remaining string, |
|
169 * new value of "last seconds"] |
|
170 */ |
|
171 _deriveTransferRate: function DU__deriveTransferRate(aCurrBytes, |
|
172 aMaxBytes, aSpeed, |
|
173 aLastSec) |
|
174 { |
|
175 if (aMaxBytes == null) |
|
176 aMaxBytes = -1; |
|
177 if (aSpeed == null) |
|
178 aSpeed = -1; |
|
179 if (aLastSec == null) |
|
180 aLastSec = Infinity; |
|
181 |
|
182 // Calculate the time remaining if we have valid values |
|
183 let seconds = (aSpeed > 0) && (aMaxBytes > 0) ? |
|
184 (aMaxBytes - aCurrBytes) / aSpeed : -1; |
|
185 |
|
186 let transfer = DownloadUtils.getTransferTotal(aCurrBytes, aMaxBytes); |
|
187 let [timeLeft, newLast] = DownloadUtils.getTimeLeft(seconds, aLastSec); |
|
188 return [transfer, timeLeft, newLast, aSpeed]; |
|
189 }, |
|
190 |
|
191 /** |
|
192 * Generate the transfer progress string to show the current and total byte |
|
193 * size. Byte units will be as large as possible and the same units for |
|
194 * current and max will be suppressed for the former. |
|
195 * |
|
196 * @param aCurrBytes |
|
197 * Number of bytes transferred so far |
|
198 * @param [optional] aMaxBytes |
|
199 * Total number of bytes or -1 for unknown |
|
200 * @return The transfer progress text |
|
201 */ |
|
202 getTransferTotal: function DU_getTransferTotal(aCurrBytes, aMaxBytes) |
|
203 { |
|
204 if (aMaxBytes == null) |
|
205 aMaxBytes = -1; |
|
206 |
|
207 let [progress, progressUnits] = DownloadUtils.convertByteUnits(aCurrBytes); |
|
208 let [total, totalUnits] = DownloadUtils.convertByteUnits(aMaxBytes); |
|
209 |
|
210 // Figure out which byte progress string to display |
|
211 let name, values; |
|
212 if (aMaxBytes < 0) { |
|
213 name = gStr.transferNoTotal; |
|
214 values = [ |
|
215 progress, |
|
216 progressUnits, |
|
217 ]; |
|
218 } else if (progressUnits == totalUnits) { |
|
219 name = gStr.transferSameUnits; |
|
220 values = [ |
|
221 progress, |
|
222 total, |
|
223 totalUnits, |
|
224 ]; |
|
225 } else { |
|
226 name = gStr.transferDiffUnits; |
|
227 values = [ |
|
228 progress, |
|
229 progressUnits, |
|
230 total, |
|
231 totalUnits, |
|
232 ]; |
|
233 } |
|
234 |
|
235 return gBundle.formatStringFromName(name, values, values.length); |
|
236 }, |
|
237 |
|
238 /** |
|
239 * Generate a "time left" string given an estimate on the time left and the |
|
240 * last time. The extra time is used to give a better estimate on the time to |
|
241 * show. Both the time values are doubles instead of integers to help get |
|
242 * sub-second accuracy for current and future estimates. |
|
243 * |
|
244 * @param aSeconds |
|
245 * Current estimate on number of seconds left for the download |
|
246 * @param [optional] aLastSec |
|
247 * Last time remaining in seconds or Infinity for unknown |
|
248 * @return A pair: [time left text, new value of "last seconds"] |
|
249 */ |
|
250 getTimeLeft: function DU_getTimeLeft(aSeconds, aLastSec) |
|
251 { |
|
252 if (aLastSec == null) |
|
253 aLastSec = Infinity; |
|
254 |
|
255 if (aSeconds < 0) |
|
256 return [gBundle.GetStringFromName(gStr.timeUnknown), aLastSec]; |
|
257 |
|
258 // Try to find a cached lastSec for the given second |
|
259 aLastSec = gCachedLast.reduce(function(aResult, aItem) |
|
260 aItem[0] == aSeconds ? aItem[1] : aResult, aLastSec); |
|
261 |
|
262 // Add the current second/lastSec pair unless we have too many |
|
263 gCachedLast.push([aSeconds, aLastSec]); |
|
264 if (gCachedLast.length > kCachedLastMaxSize) |
|
265 gCachedLast.shift(); |
|
266 |
|
267 // Apply smoothing only if the new time isn't a huge change -- e.g., if the |
|
268 // new time is more than half the previous time; this is useful for |
|
269 // downloads that start/resume slowly |
|
270 if (aSeconds > aLastSec / 2) { |
|
271 // Apply hysteresis to favor downward over upward swings |
|
272 // 30% of down and 10% of up (exponential smoothing) |
|
273 let (diff = aSeconds - aLastSec) { |
|
274 aSeconds = aLastSec + (diff < 0 ? .3 : .1) * diff; |
|
275 } |
|
276 |
|
277 // If the new time is similar, reuse something close to the last seconds, |
|
278 // but subtract a little to provide forward progress |
|
279 let diff = aSeconds - aLastSec; |
|
280 let diffPct = diff / aLastSec * 100; |
|
281 if (Math.abs(diff) < 5 || Math.abs(diffPct) < 5) |
|
282 aSeconds = aLastSec - (diff < 0 ? .4 : .2); |
|
283 } |
|
284 |
|
285 // Decide what text to show for the time |
|
286 let timeLeft; |
|
287 if (aSeconds < 4) { |
|
288 // Be friendly in the last few seconds |
|
289 timeLeft = gBundle.GetStringFromName(gStr.timeFewSeconds); |
|
290 } else { |
|
291 // Convert the seconds into its two largest units to display |
|
292 let [time1, unit1, time2, unit2] = |
|
293 DownloadUtils.convertTimeUnits(aSeconds); |
|
294 |
|
295 let pair1 = |
|
296 gBundle.formatStringFromName(gStr.timePair, [time1, unit1], 2); |
|
297 let pair2 = |
|
298 gBundle.formatStringFromName(gStr.timePair, [time2, unit2], 2); |
|
299 |
|
300 // Only show minutes for under 1 hour unless there's a few minutes left; |
|
301 // or the second pair is 0. |
|
302 if ((aSeconds < 3600 && time1 >= 4) || time2 == 0) { |
|
303 timeLeft = gBundle.formatStringFromName(gStr.timeLeftSingle, |
|
304 [pair1], 1); |
|
305 } else { |
|
306 // We've got 2 pairs of times to display |
|
307 timeLeft = gBundle.formatStringFromName(gStr.timeLeftDouble, |
|
308 [pair1, pair2], 2); |
|
309 } |
|
310 } |
|
311 |
|
312 return [timeLeft, aSeconds]; |
|
313 }, |
|
314 |
|
315 /** |
|
316 * Converts a Date object to two readable formats, one compact, one complete. |
|
317 * The compact format is relative to the current date, and is not an accurate |
|
318 * representation. For example, only the time is displayed for today. The |
|
319 * complete format always includes both the date and the time, excluding the |
|
320 * seconds, and is often shown when hovering the cursor over the compact |
|
321 * representation. |
|
322 * |
|
323 * @param aDate |
|
324 * Date object representing the date and time to format. It is assumed |
|
325 * that this value represents a past date. |
|
326 * @param [optional] aNow |
|
327 * Date object representing the current date and time. The real date |
|
328 * and time of invocation is used if this parameter is omitted. |
|
329 * @return A pair: [compact text, complete text] |
|
330 */ |
|
331 getReadableDates: function DU_getReadableDates(aDate, aNow) |
|
332 { |
|
333 if (!aNow) { |
|
334 aNow = new Date(); |
|
335 } |
|
336 |
|
337 let dts = Cc["@mozilla.org/intl/scriptabledateformat;1"] |
|
338 .getService(Ci.nsIScriptableDateFormat); |
|
339 |
|
340 // Figure out when today begins |
|
341 let today = new Date(aNow.getFullYear(), aNow.getMonth(), aNow.getDate()); |
|
342 |
|
343 // Figure out if the time is from today, yesterday, this week, etc. |
|
344 let dateTimeCompact; |
|
345 if (aDate >= today) { |
|
346 // After today started, show the time |
|
347 dateTimeCompact = dts.FormatTime("", |
|
348 dts.timeFormatNoSeconds, |
|
349 aDate.getHours(), |
|
350 aDate.getMinutes(), |
|
351 0); |
|
352 } else if (today - aDate < (24 * 60 * 60 * 1000)) { |
|
353 // After yesterday started, show yesterday |
|
354 dateTimeCompact = gBundle.GetStringFromName(gStr.yesterday); |
|
355 } else if (today - aDate < (6 * 24 * 60 * 60 * 1000)) { |
|
356 // After last week started, show day of week |
|
357 dateTimeCompact = aDate.toLocaleFormat("%A"); |
|
358 } else { |
|
359 // Show month/day |
|
360 let month = aDate.toLocaleFormat("%B"); |
|
361 // Remove leading 0 by converting the date string to a number |
|
362 let date = Number(aDate.toLocaleFormat("%d")); |
|
363 dateTimeCompact = gBundle.formatStringFromName(gStr.monthDate, [month, date], 2); |
|
364 } |
|
365 |
|
366 let dateTimeFull = dts.FormatDateTime("", |
|
367 dts.dateFormatLong, |
|
368 dts.timeFormatNoSeconds, |
|
369 aDate.getFullYear(), |
|
370 aDate.getMonth() + 1, |
|
371 aDate.getDate(), |
|
372 aDate.getHours(), |
|
373 aDate.getMinutes(), |
|
374 0); |
|
375 |
|
376 return [dateTimeCompact, dateTimeFull]; |
|
377 }, |
|
378 |
|
379 /** |
|
380 * Get the appropriate display host string for a URI string depending on if |
|
381 * the URI has an eTLD + 1, is an IP address, a local file, or other protocol |
|
382 * |
|
383 * @param aURIString |
|
384 * The URI string to try getting an eTLD + 1, etc. |
|
385 * @return A pair: [display host for the URI string, full host name] |
|
386 */ |
|
387 getURIHost: function DU_getURIHost(aURIString) |
|
388 { |
|
389 let ioService = Cc["@mozilla.org/network/io-service;1"]. |
|
390 getService(Ci.nsIIOService); |
|
391 let eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"]. |
|
392 getService(Ci.nsIEffectiveTLDService); |
|
393 let idnService = Cc["@mozilla.org/network/idn-service;1"]. |
|
394 getService(Ci.nsIIDNService); |
|
395 |
|
396 // Get a URI that knows about its components |
|
397 let uri = ioService.newURI(aURIString, null, null); |
|
398 |
|
399 // Get the inner-most uri for schemes like jar: |
|
400 if (uri instanceof Ci.nsINestedURI) |
|
401 uri = uri.innermostURI; |
|
402 |
|
403 let fullHost; |
|
404 try { |
|
405 // Get the full host name; some special URIs fail (data: jar:) |
|
406 fullHost = uri.host; |
|
407 } catch (e) { |
|
408 fullHost = ""; |
|
409 } |
|
410 |
|
411 let displayHost; |
|
412 try { |
|
413 // This might fail if it's an IP address or doesn't have more than 1 part |
|
414 let baseDomain = eTLDService.getBaseDomain(uri); |
|
415 |
|
416 // Convert base domain for display; ignore the isAscii out param |
|
417 displayHost = idnService.convertToDisplayIDN(baseDomain, {}); |
|
418 } catch (e) { |
|
419 // Default to the host name |
|
420 displayHost = fullHost; |
|
421 } |
|
422 |
|
423 // Check if we need to show something else for the host |
|
424 if (uri.scheme == "file") { |
|
425 // Display special text for file protocol |
|
426 displayHost = gBundle.GetStringFromName(gStr.doneFileScheme); |
|
427 fullHost = displayHost; |
|
428 } else if (displayHost.length == 0) { |
|
429 // Got nothing; show the scheme (data: about: moz-icon:) |
|
430 displayHost = |
|
431 gBundle.formatStringFromName(gStr.doneScheme, [uri.scheme], 1); |
|
432 fullHost = displayHost; |
|
433 } else if (uri.port != -1) { |
|
434 // Tack on the port if it's not the default port |
|
435 let port = ":" + uri.port; |
|
436 displayHost += port; |
|
437 fullHost += port; |
|
438 } |
|
439 |
|
440 return [displayHost, fullHost]; |
|
441 }, |
|
442 |
|
443 /** |
|
444 * Converts a number of bytes to the appropriate unit that results in an |
|
445 * internationalized number that needs fewer than 4 digits. |
|
446 * |
|
447 * @param aBytes |
|
448 * Number of bytes to convert |
|
449 * @return A pair: [new value with 3 sig. figs., its unit] |
|
450 */ |
|
451 convertByteUnits: function DU_convertByteUnits(aBytes) |
|
452 { |
|
453 let unitIndex = 0; |
|
454 |
|
455 // Convert to next unit if it needs 4 digits (after rounding), but only if |
|
456 // we know the name of the next unit |
|
457 while ((aBytes >= 999.5) && (unitIndex < gStr.units.length - 1)) { |
|
458 aBytes /= 1024; |
|
459 unitIndex++; |
|
460 } |
|
461 |
|
462 // Get rid of insignificant bits by truncating to 1 or 0 decimal points |
|
463 // 0 -> 0; 1.2 -> 1.2; 12.3 -> 12.3; 123.4 -> 123; 234.5 -> 235 |
|
464 // added in bug 462064: (unitIndex != 0) makes sure that no decimal digit for bytes appears when aBytes < 100 |
|
465 aBytes = aBytes.toFixed((aBytes > 0) && (aBytes < 100) && (unitIndex != 0) ? 1 : 0); |
|
466 |
|
467 if (gDecimalSymbol != ".") |
|
468 aBytes = aBytes.replace(".", gDecimalSymbol); |
|
469 return [aBytes, gBundle.GetStringFromName(gStr.units[unitIndex])]; |
|
470 }, |
|
471 |
|
472 /** |
|
473 * Converts a number of seconds to the two largest units. Time values are |
|
474 * whole numbers, and units have the correct plural/singular form. |
|
475 * |
|
476 * @param aSecs |
|
477 * Seconds to convert into the appropriate 2 units |
|
478 * @return 4-item array [first value, its unit, second value, its unit] |
|
479 */ |
|
480 convertTimeUnits: function DU_convertTimeUnits(aSecs) |
|
481 { |
|
482 // These are the maximum values for seconds, minutes, hours corresponding |
|
483 // with gStr.timeUnits without the last item |
|
484 let timeSize = [60, 60, 24]; |
|
485 |
|
486 let time = aSecs; |
|
487 let scale = 1; |
|
488 let unitIndex = 0; |
|
489 |
|
490 // Keep converting to the next unit while we have units left and the |
|
491 // current one isn't the largest unit possible |
|
492 while ((unitIndex < timeSize.length) && (time >= timeSize[unitIndex])) { |
|
493 time /= timeSize[unitIndex]; |
|
494 scale *= timeSize[unitIndex]; |
|
495 unitIndex++; |
|
496 } |
|
497 |
|
498 let value = convertTimeUnitsValue(time); |
|
499 let units = convertTimeUnitsUnits(value, unitIndex); |
|
500 |
|
501 let extra = aSecs - value * scale; |
|
502 let nextIndex = unitIndex - 1; |
|
503 |
|
504 // Convert the extra time to the next largest unit |
|
505 for (let index = 0; index < nextIndex; index++) |
|
506 extra /= timeSize[index]; |
|
507 |
|
508 let value2 = convertTimeUnitsValue(extra); |
|
509 let units2 = convertTimeUnitsUnits(value2, nextIndex); |
|
510 |
|
511 return [value, units, value2, units2]; |
|
512 }, |
|
513 }; |
|
514 |
|
515 /** |
|
516 * Private helper for convertTimeUnits that gets the display value of a time |
|
517 * |
|
518 * @param aTime |
|
519 * Time value for display |
|
520 * @return An integer value for the time rounded down |
|
521 */ |
|
522 function convertTimeUnitsValue(aTime) |
|
523 { |
|
524 return Math.floor(aTime); |
|
525 } |
|
526 |
|
527 /** |
|
528 * Private helper for convertTimeUnits that gets the display units of a time |
|
529 * |
|
530 * @param aTime |
|
531 * Time value for display |
|
532 * @param aIndex |
|
533 * Index into gStr.timeUnits for the appropriate unit |
|
534 * @return The appropriate plural form of the unit for the time |
|
535 */ |
|
536 function convertTimeUnitsUnits(aTime, aIndex) |
|
537 { |
|
538 // Negative index would be an invalid unit, so just give empty |
|
539 if (aIndex < 0) |
|
540 return ""; |
|
541 |
|
542 return PluralForm.get(aTime, gBundle.GetStringFromName(gStr.timeUnits[aIndex])); |
|
543 } |
|
544 |
|
545 /** |
|
546 * Private helper function to log errors to the error console and command line |
|
547 * |
|
548 * @param aMsg |
|
549 * Error message to log or an array of strings to concat |
|
550 */ |
|
551 function log(aMsg) |
|
552 { |
|
553 let msg = "DownloadUtils.jsm: " + (aMsg.join ? aMsg.join("") : aMsg); |
|
554 Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService). |
|
555 logStringMessage(msg); |
|
556 dump(msg + "\n"); |
|
557 } |