1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/toolkit/mozapps/downloads/DownloadUtils.jsm Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,557 @@ 1.4 +/* vim: sw=2 ts=2 sts=2 expandtab filetype=javascript 1.5 + * This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +this.EXPORTED_SYMBOLS = [ "DownloadUtils" ]; 1.10 + 1.11 +/** 1.12 + * This module provides the DownloadUtils object which contains useful methods 1.13 + * for downloads such as displaying file sizes, transfer times, and download 1.14 + * locations. 1.15 + * 1.16 + * List of methods: 1.17 + * 1.18 + * [string status, double newLast] 1.19 + * getDownloadStatus(int aCurrBytes, [optional] int aMaxBytes, 1.20 + * [optional] double aSpeed, [optional] double aLastSec) 1.21 + * 1.22 + * string progress 1.23 + * getTransferTotal(int aCurrBytes, [optional] int aMaxBytes) 1.24 + * 1.25 + * [string timeLeft, double newLast] 1.26 + * getTimeLeft(double aSeconds, [optional] double aLastSec) 1.27 + * 1.28 + * [string dateCompact, string dateComplete] 1.29 + * getReadableDates(Date aDate, [optional] Date aNow) 1.30 + * 1.31 + * [string displayHost, string fullHost] 1.32 + * getURIHost(string aURIString) 1.33 + * 1.34 + * [string convertedBytes, string units] 1.35 + * convertByteUnits(int aBytes) 1.36 + * 1.37 + * [int time, string units, int subTime, string subUnits] 1.38 + * convertTimeUnits(double aSecs) 1.39 + */ 1.40 + 1.41 +const Cc = Components.classes; 1.42 +const Ci = Components.interfaces; 1.43 +const Cu = Components.utils; 1.44 + 1.45 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.46 + 1.47 +XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", 1.48 + "resource://gre/modules/PluralForm.jsm"); 1.49 + 1.50 +this.__defineGetter__("gDecimalSymbol", function() { 1.51 + delete this.gDecimalSymbol; 1.52 + return this.gDecimalSymbol = Number(5.4).toLocaleString().match(/\D/); 1.53 +}); 1.54 + 1.55 +const kDownloadProperties = 1.56 + "chrome://mozapps/locale/downloads/downloads.properties"; 1.57 + 1.58 +let gStr = { 1.59 + statusFormat: "statusFormat3", 1.60 + statusFormatInfiniteRate: "statusFormatInfiniteRate", 1.61 + statusFormatNoRate: "statusFormatNoRate", 1.62 + transferSameUnits: "transferSameUnits2", 1.63 + transferDiffUnits: "transferDiffUnits2", 1.64 + transferNoTotal: "transferNoTotal2", 1.65 + timePair: "timePair2", 1.66 + timeLeftSingle: "timeLeftSingle2", 1.67 + timeLeftDouble: "timeLeftDouble2", 1.68 + timeFewSeconds: "timeFewSeconds", 1.69 + timeUnknown: "timeUnknown", 1.70 + monthDate: "monthDate2", 1.71 + yesterday: "yesterday", 1.72 + doneScheme: "doneScheme2", 1.73 + doneFileScheme: "doneFileScheme", 1.74 + units: ["bytes", "kilobyte", "megabyte", "gigabyte"], 1.75 + // Update timeSize in convertTimeUnits if changing the length of this array 1.76 + timeUnits: ["seconds", "minutes", "hours", "days"], 1.77 + infiniteRate: "infiniteRate", 1.78 +}; 1.79 + 1.80 +// This lazily initializes the string bundle upon first use. 1.81 +this.__defineGetter__("gBundle", function() { 1.82 + delete gBundle; 1.83 + return this.gBundle = Cc["@mozilla.org/intl/stringbundle;1"]. 1.84 + getService(Ci.nsIStringBundleService). 1.85 + createBundle(kDownloadProperties); 1.86 +}); 1.87 + 1.88 +// Keep track of at most this many second/lastSec pairs so that multiple calls 1.89 +// to getTimeLeft produce the same time left 1.90 +const kCachedLastMaxSize = 10; 1.91 +let gCachedLast = []; 1.92 + 1.93 +this.DownloadUtils = { 1.94 + /** 1.95 + * Generate a full status string for a download given its current progress, 1.96 + * total size, speed, last time remaining 1.97 + * 1.98 + * @param aCurrBytes 1.99 + * Number of bytes transferred so far 1.100 + * @param [optional] aMaxBytes 1.101 + * Total number of bytes or -1 for unknown 1.102 + * @param [optional] aSpeed 1.103 + * Current transfer rate in bytes/sec or -1 for unknown 1.104 + * @param [optional] aLastSec 1.105 + * Last time remaining in seconds or Infinity for unknown 1.106 + * @return A pair: [download status text, new value of "last seconds"] 1.107 + */ 1.108 + getDownloadStatus: function DU_getDownloadStatus(aCurrBytes, aMaxBytes, 1.109 + aSpeed, aLastSec) 1.110 + { 1.111 + let [transfer, timeLeft, newLast, normalizedSpeed] 1.112 + = this._deriveTransferRate(aCurrBytes, aMaxBytes, aSpeed, aLastSec); 1.113 + 1.114 + let [rate, unit] = DownloadUtils.convertByteUnits(normalizedSpeed); 1.115 + 1.116 + let status; 1.117 + if (rate === "Infinity") { 1.118 + // Infinity download speed doesn't make sense. Show a localized phrase instead. 1.119 + let params = [transfer, gBundle.GetStringFromName(gStr.infiniteRate), timeLeft]; 1.120 + status = gBundle.formatStringFromName(gStr.statusFormatInfiniteRate, params, 1.121 + params.length); 1.122 + } 1.123 + else { 1.124 + let params = [transfer, rate, unit, timeLeft]; 1.125 + status = gBundle.formatStringFromName(gStr.statusFormat, params, 1.126 + params.length); 1.127 + } 1.128 + return [status, newLast]; 1.129 + }, 1.130 + 1.131 + /** 1.132 + * Generate a status string for a download given its current progress, 1.133 + * total size, speed, last time remaining. The status string contains the 1.134 + * time remaining, as well as the total bytes downloaded. Unlike 1.135 + * getDownloadStatus, it does not include the rate of download. 1.136 + * 1.137 + * @param aCurrBytes 1.138 + * Number of bytes transferred so far 1.139 + * @param [optional] aMaxBytes 1.140 + * Total number of bytes or -1 for unknown 1.141 + * @param [optional] aSpeed 1.142 + * Current transfer rate in bytes/sec or -1 for unknown 1.143 + * @param [optional] aLastSec 1.144 + * Last time remaining in seconds or Infinity for unknown 1.145 + * @return A pair: [download status text, new value of "last seconds"] 1.146 + */ 1.147 + getDownloadStatusNoRate: 1.148 + function DU_getDownloadStatusNoRate(aCurrBytes, aMaxBytes, aSpeed, 1.149 + aLastSec) 1.150 + { 1.151 + let [transfer, timeLeft, newLast] 1.152 + = this._deriveTransferRate(aCurrBytes, aMaxBytes, aSpeed, aLastSec); 1.153 + 1.154 + let params = [transfer, timeLeft]; 1.155 + let status = gBundle.formatStringFromName(gStr.statusFormatNoRate, params, 1.156 + params.length); 1.157 + return [status, newLast]; 1.158 + }, 1.159 + 1.160 + /** 1.161 + * Helper function that returns a transfer string, a time remaining string, 1.162 + * and a new value of "last seconds". 1.163 + * @param aCurrBytes 1.164 + * Number of bytes transferred so far 1.165 + * @param [optional] aMaxBytes 1.166 + * Total number of bytes or -1 for unknown 1.167 + * @param [optional] aSpeed 1.168 + * Current transfer rate in bytes/sec or -1 for unknown 1.169 + * @param [optional] aLastSec 1.170 + * Last time remaining in seconds or Infinity for unknown 1.171 + * @return A triple: [amount transferred string, time remaining string, 1.172 + * new value of "last seconds"] 1.173 + */ 1.174 + _deriveTransferRate: function DU__deriveTransferRate(aCurrBytes, 1.175 + aMaxBytes, aSpeed, 1.176 + aLastSec) 1.177 + { 1.178 + if (aMaxBytes == null) 1.179 + aMaxBytes = -1; 1.180 + if (aSpeed == null) 1.181 + aSpeed = -1; 1.182 + if (aLastSec == null) 1.183 + aLastSec = Infinity; 1.184 + 1.185 + // Calculate the time remaining if we have valid values 1.186 + let seconds = (aSpeed > 0) && (aMaxBytes > 0) ? 1.187 + (aMaxBytes - aCurrBytes) / aSpeed : -1; 1.188 + 1.189 + let transfer = DownloadUtils.getTransferTotal(aCurrBytes, aMaxBytes); 1.190 + let [timeLeft, newLast] = DownloadUtils.getTimeLeft(seconds, aLastSec); 1.191 + return [transfer, timeLeft, newLast, aSpeed]; 1.192 + }, 1.193 + 1.194 + /** 1.195 + * Generate the transfer progress string to show the current and total byte 1.196 + * size. Byte units will be as large as possible and the same units for 1.197 + * current and max will be suppressed for the former. 1.198 + * 1.199 + * @param aCurrBytes 1.200 + * Number of bytes transferred so far 1.201 + * @param [optional] aMaxBytes 1.202 + * Total number of bytes or -1 for unknown 1.203 + * @return The transfer progress text 1.204 + */ 1.205 + getTransferTotal: function DU_getTransferTotal(aCurrBytes, aMaxBytes) 1.206 + { 1.207 + if (aMaxBytes == null) 1.208 + aMaxBytes = -1; 1.209 + 1.210 + let [progress, progressUnits] = DownloadUtils.convertByteUnits(aCurrBytes); 1.211 + let [total, totalUnits] = DownloadUtils.convertByteUnits(aMaxBytes); 1.212 + 1.213 + // Figure out which byte progress string to display 1.214 + let name, values; 1.215 + if (aMaxBytes < 0) { 1.216 + name = gStr.transferNoTotal; 1.217 + values = [ 1.218 + progress, 1.219 + progressUnits, 1.220 + ]; 1.221 + } else if (progressUnits == totalUnits) { 1.222 + name = gStr.transferSameUnits; 1.223 + values = [ 1.224 + progress, 1.225 + total, 1.226 + totalUnits, 1.227 + ]; 1.228 + } else { 1.229 + name = gStr.transferDiffUnits; 1.230 + values = [ 1.231 + progress, 1.232 + progressUnits, 1.233 + total, 1.234 + totalUnits, 1.235 + ]; 1.236 + } 1.237 + 1.238 + return gBundle.formatStringFromName(name, values, values.length); 1.239 + }, 1.240 + 1.241 + /** 1.242 + * Generate a "time left" string given an estimate on the time left and the 1.243 + * last time. The extra time is used to give a better estimate on the time to 1.244 + * show. Both the time values are doubles instead of integers to help get 1.245 + * sub-second accuracy for current and future estimates. 1.246 + * 1.247 + * @param aSeconds 1.248 + * Current estimate on number of seconds left for the download 1.249 + * @param [optional] aLastSec 1.250 + * Last time remaining in seconds or Infinity for unknown 1.251 + * @return A pair: [time left text, new value of "last seconds"] 1.252 + */ 1.253 + getTimeLeft: function DU_getTimeLeft(aSeconds, aLastSec) 1.254 + { 1.255 + if (aLastSec == null) 1.256 + aLastSec = Infinity; 1.257 + 1.258 + if (aSeconds < 0) 1.259 + return [gBundle.GetStringFromName(gStr.timeUnknown), aLastSec]; 1.260 + 1.261 + // Try to find a cached lastSec for the given second 1.262 + aLastSec = gCachedLast.reduce(function(aResult, aItem) 1.263 + aItem[0] == aSeconds ? aItem[1] : aResult, aLastSec); 1.264 + 1.265 + // Add the current second/lastSec pair unless we have too many 1.266 + gCachedLast.push([aSeconds, aLastSec]); 1.267 + if (gCachedLast.length > kCachedLastMaxSize) 1.268 + gCachedLast.shift(); 1.269 + 1.270 + // Apply smoothing only if the new time isn't a huge change -- e.g., if the 1.271 + // new time is more than half the previous time; this is useful for 1.272 + // downloads that start/resume slowly 1.273 + if (aSeconds > aLastSec / 2) { 1.274 + // Apply hysteresis to favor downward over upward swings 1.275 + // 30% of down and 10% of up (exponential smoothing) 1.276 + let (diff = aSeconds - aLastSec) { 1.277 + aSeconds = aLastSec + (diff < 0 ? .3 : .1) * diff; 1.278 + } 1.279 + 1.280 + // If the new time is similar, reuse something close to the last seconds, 1.281 + // but subtract a little to provide forward progress 1.282 + let diff = aSeconds - aLastSec; 1.283 + let diffPct = diff / aLastSec * 100; 1.284 + if (Math.abs(diff) < 5 || Math.abs(diffPct) < 5) 1.285 + aSeconds = aLastSec - (diff < 0 ? .4 : .2); 1.286 + } 1.287 + 1.288 + // Decide what text to show for the time 1.289 + let timeLeft; 1.290 + if (aSeconds < 4) { 1.291 + // Be friendly in the last few seconds 1.292 + timeLeft = gBundle.GetStringFromName(gStr.timeFewSeconds); 1.293 + } else { 1.294 + // Convert the seconds into its two largest units to display 1.295 + let [time1, unit1, time2, unit2] = 1.296 + DownloadUtils.convertTimeUnits(aSeconds); 1.297 + 1.298 + let pair1 = 1.299 + gBundle.formatStringFromName(gStr.timePair, [time1, unit1], 2); 1.300 + let pair2 = 1.301 + gBundle.formatStringFromName(gStr.timePair, [time2, unit2], 2); 1.302 + 1.303 + // Only show minutes for under 1 hour unless there's a few minutes left; 1.304 + // or the second pair is 0. 1.305 + if ((aSeconds < 3600 && time1 >= 4) || time2 == 0) { 1.306 + timeLeft = gBundle.formatStringFromName(gStr.timeLeftSingle, 1.307 + [pair1], 1); 1.308 + } else { 1.309 + // We've got 2 pairs of times to display 1.310 + timeLeft = gBundle.formatStringFromName(gStr.timeLeftDouble, 1.311 + [pair1, pair2], 2); 1.312 + } 1.313 + } 1.314 + 1.315 + return [timeLeft, aSeconds]; 1.316 + }, 1.317 + 1.318 + /** 1.319 + * Converts a Date object to two readable formats, one compact, one complete. 1.320 + * The compact format is relative to the current date, and is not an accurate 1.321 + * representation. For example, only the time is displayed for today. The 1.322 + * complete format always includes both the date and the time, excluding the 1.323 + * seconds, and is often shown when hovering the cursor over the compact 1.324 + * representation. 1.325 + * 1.326 + * @param aDate 1.327 + * Date object representing the date and time to format. It is assumed 1.328 + * that this value represents a past date. 1.329 + * @param [optional] aNow 1.330 + * Date object representing the current date and time. The real date 1.331 + * and time of invocation is used if this parameter is omitted. 1.332 + * @return A pair: [compact text, complete text] 1.333 + */ 1.334 + getReadableDates: function DU_getReadableDates(aDate, aNow) 1.335 + { 1.336 + if (!aNow) { 1.337 + aNow = new Date(); 1.338 + } 1.339 + 1.340 + let dts = Cc["@mozilla.org/intl/scriptabledateformat;1"] 1.341 + .getService(Ci.nsIScriptableDateFormat); 1.342 + 1.343 + // Figure out when today begins 1.344 + let today = new Date(aNow.getFullYear(), aNow.getMonth(), aNow.getDate()); 1.345 + 1.346 + // Figure out if the time is from today, yesterday, this week, etc. 1.347 + let dateTimeCompact; 1.348 + if (aDate >= today) { 1.349 + // After today started, show the time 1.350 + dateTimeCompact = dts.FormatTime("", 1.351 + dts.timeFormatNoSeconds, 1.352 + aDate.getHours(), 1.353 + aDate.getMinutes(), 1.354 + 0); 1.355 + } else if (today - aDate < (24 * 60 * 60 * 1000)) { 1.356 + // After yesterday started, show yesterday 1.357 + dateTimeCompact = gBundle.GetStringFromName(gStr.yesterday); 1.358 + } else if (today - aDate < (6 * 24 * 60 * 60 * 1000)) { 1.359 + // After last week started, show day of week 1.360 + dateTimeCompact = aDate.toLocaleFormat("%A"); 1.361 + } else { 1.362 + // Show month/day 1.363 + let month = aDate.toLocaleFormat("%B"); 1.364 + // Remove leading 0 by converting the date string to a number 1.365 + let date = Number(aDate.toLocaleFormat("%d")); 1.366 + dateTimeCompact = gBundle.formatStringFromName(gStr.monthDate, [month, date], 2); 1.367 + } 1.368 + 1.369 + let dateTimeFull = dts.FormatDateTime("", 1.370 + dts.dateFormatLong, 1.371 + dts.timeFormatNoSeconds, 1.372 + aDate.getFullYear(), 1.373 + aDate.getMonth() + 1, 1.374 + aDate.getDate(), 1.375 + aDate.getHours(), 1.376 + aDate.getMinutes(), 1.377 + 0); 1.378 + 1.379 + return [dateTimeCompact, dateTimeFull]; 1.380 + }, 1.381 + 1.382 + /** 1.383 + * Get the appropriate display host string for a URI string depending on if 1.384 + * the URI has an eTLD + 1, is an IP address, a local file, or other protocol 1.385 + * 1.386 + * @param aURIString 1.387 + * The URI string to try getting an eTLD + 1, etc. 1.388 + * @return A pair: [display host for the URI string, full host name] 1.389 + */ 1.390 + getURIHost: function DU_getURIHost(aURIString) 1.391 + { 1.392 + let ioService = Cc["@mozilla.org/network/io-service;1"]. 1.393 + getService(Ci.nsIIOService); 1.394 + let eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"]. 1.395 + getService(Ci.nsIEffectiveTLDService); 1.396 + let idnService = Cc["@mozilla.org/network/idn-service;1"]. 1.397 + getService(Ci.nsIIDNService); 1.398 + 1.399 + // Get a URI that knows about its components 1.400 + let uri = ioService.newURI(aURIString, null, null); 1.401 + 1.402 + // Get the inner-most uri for schemes like jar: 1.403 + if (uri instanceof Ci.nsINestedURI) 1.404 + uri = uri.innermostURI; 1.405 + 1.406 + let fullHost; 1.407 + try { 1.408 + // Get the full host name; some special URIs fail (data: jar:) 1.409 + fullHost = uri.host; 1.410 + } catch (e) { 1.411 + fullHost = ""; 1.412 + } 1.413 + 1.414 + let displayHost; 1.415 + try { 1.416 + // This might fail if it's an IP address or doesn't have more than 1 part 1.417 + let baseDomain = eTLDService.getBaseDomain(uri); 1.418 + 1.419 + // Convert base domain for display; ignore the isAscii out param 1.420 + displayHost = idnService.convertToDisplayIDN(baseDomain, {}); 1.421 + } catch (e) { 1.422 + // Default to the host name 1.423 + displayHost = fullHost; 1.424 + } 1.425 + 1.426 + // Check if we need to show something else for the host 1.427 + if (uri.scheme == "file") { 1.428 + // Display special text for file protocol 1.429 + displayHost = gBundle.GetStringFromName(gStr.doneFileScheme); 1.430 + fullHost = displayHost; 1.431 + } else if (displayHost.length == 0) { 1.432 + // Got nothing; show the scheme (data: about: moz-icon:) 1.433 + displayHost = 1.434 + gBundle.formatStringFromName(gStr.doneScheme, [uri.scheme], 1); 1.435 + fullHost = displayHost; 1.436 + } else if (uri.port != -1) { 1.437 + // Tack on the port if it's not the default port 1.438 + let port = ":" + uri.port; 1.439 + displayHost += port; 1.440 + fullHost += port; 1.441 + } 1.442 + 1.443 + return [displayHost, fullHost]; 1.444 + }, 1.445 + 1.446 + /** 1.447 + * Converts a number of bytes to the appropriate unit that results in an 1.448 + * internationalized number that needs fewer than 4 digits. 1.449 + * 1.450 + * @param aBytes 1.451 + * Number of bytes to convert 1.452 + * @return A pair: [new value with 3 sig. figs., its unit] 1.453 + */ 1.454 + convertByteUnits: function DU_convertByteUnits(aBytes) 1.455 + { 1.456 + let unitIndex = 0; 1.457 + 1.458 + // Convert to next unit if it needs 4 digits (after rounding), but only if 1.459 + // we know the name of the next unit 1.460 + while ((aBytes >= 999.5) && (unitIndex < gStr.units.length - 1)) { 1.461 + aBytes /= 1024; 1.462 + unitIndex++; 1.463 + } 1.464 + 1.465 + // Get rid of insignificant bits by truncating to 1 or 0 decimal points 1.466 + // 0 -> 0; 1.2 -> 1.2; 12.3 -> 12.3; 123.4 -> 123; 234.5 -> 235 1.467 + // added in bug 462064: (unitIndex != 0) makes sure that no decimal digit for bytes appears when aBytes < 100 1.468 + aBytes = aBytes.toFixed((aBytes > 0) && (aBytes < 100) && (unitIndex != 0) ? 1 : 0); 1.469 + 1.470 + if (gDecimalSymbol != ".") 1.471 + aBytes = aBytes.replace(".", gDecimalSymbol); 1.472 + return [aBytes, gBundle.GetStringFromName(gStr.units[unitIndex])]; 1.473 + }, 1.474 + 1.475 + /** 1.476 + * Converts a number of seconds to the two largest units. Time values are 1.477 + * whole numbers, and units have the correct plural/singular form. 1.478 + * 1.479 + * @param aSecs 1.480 + * Seconds to convert into the appropriate 2 units 1.481 + * @return 4-item array [first value, its unit, second value, its unit] 1.482 + */ 1.483 + convertTimeUnits: function DU_convertTimeUnits(aSecs) 1.484 + { 1.485 + // These are the maximum values for seconds, minutes, hours corresponding 1.486 + // with gStr.timeUnits without the last item 1.487 + let timeSize = [60, 60, 24]; 1.488 + 1.489 + let time = aSecs; 1.490 + let scale = 1; 1.491 + let unitIndex = 0; 1.492 + 1.493 + // Keep converting to the next unit while we have units left and the 1.494 + // current one isn't the largest unit possible 1.495 + while ((unitIndex < timeSize.length) && (time >= timeSize[unitIndex])) { 1.496 + time /= timeSize[unitIndex]; 1.497 + scale *= timeSize[unitIndex]; 1.498 + unitIndex++; 1.499 + } 1.500 + 1.501 + let value = convertTimeUnitsValue(time); 1.502 + let units = convertTimeUnitsUnits(value, unitIndex); 1.503 + 1.504 + let extra = aSecs - value * scale; 1.505 + let nextIndex = unitIndex - 1; 1.506 + 1.507 + // Convert the extra time to the next largest unit 1.508 + for (let index = 0; index < nextIndex; index++) 1.509 + extra /= timeSize[index]; 1.510 + 1.511 + let value2 = convertTimeUnitsValue(extra); 1.512 + let units2 = convertTimeUnitsUnits(value2, nextIndex); 1.513 + 1.514 + return [value, units, value2, units2]; 1.515 + }, 1.516 +}; 1.517 + 1.518 +/** 1.519 + * Private helper for convertTimeUnits that gets the display value of a time 1.520 + * 1.521 + * @param aTime 1.522 + * Time value for display 1.523 + * @return An integer value for the time rounded down 1.524 + */ 1.525 +function convertTimeUnitsValue(aTime) 1.526 +{ 1.527 + return Math.floor(aTime); 1.528 +} 1.529 + 1.530 +/** 1.531 + * Private helper for convertTimeUnits that gets the display units of a time 1.532 + * 1.533 + * @param aTime 1.534 + * Time value for display 1.535 + * @param aIndex 1.536 + * Index into gStr.timeUnits for the appropriate unit 1.537 + * @return The appropriate plural form of the unit for the time 1.538 + */ 1.539 +function convertTimeUnitsUnits(aTime, aIndex) 1.540 +{ 1.541 + // Negative index would be an invalid unit, so just give empty 1.542 + if (aIndex < 0) 1.543 + return ""; 1.544 + 1.545 + return PluralForm.get(aTime, gBundle.GetStringFromName(gStr.timeUnits[aIndex])); 1.546 +} 1.547 + 1.548 +/** 1.549 + * Private helper function to log errors to the error console and command line 1.550 + * 1.551 + * @param aMsg 1.552 + * Error message to log or an array of strings to concat 1.553 + */ 1.554 +function log(aMsg) 1.555 +{ 1.556 + let msg = "DownloadUtils.jsm: " + (aMsg.join ? aMsg.join("") : aMsg); 1.557 + Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService). 1.558 + logStringMessage(msg); 1.559 + dump(msg + "\n"); 1.560 +}