toolkit/mozapps/downloads/DownloadUtils.jsm

changeset 0
6474c204b198
     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 +}

mercurial