Monitoring downloads

Obsolete
This feature is obsolete. Although it may still work in some browsers, its use is discouraged since it could be removed at any time. Try to avoid using it.

Warning: This interface is deprecated as of Firefox 26. Please use Downloads.jsm instead.

Firefox 3 makes it easier than ever to monitor the status of downloads. Although it was possible to do so in previous versions of Firefox, it was previously only possible for one observer to do so at a time. Firefox 3 introduces new API that allows any number of listeners to observe downloads.

This article demonstrates how to monitor downloads in Firefox 3, using the Download Manager. As a nice bonus, it also demonstrates how to use the Storage API to issue sqlite commands on a database. The result is a window you can open by choosing "Download log" in the Tools menu, which lists all downloads that have been started since you installed the extension. In the list is the name of the file, the start and end times of the download, the download speed, and the status of the download. A tooltip is included that displays the full source URL of the file.

Download the full sample.

Setting up

When the extension loads, it will do some housekeeping chores. In particular, it needs to get an instance of the Download Manager's nsIDownloadManager interface and create the database into which its data will be stored.

 onLoad: function() {
   // initialization code
   this.initialized = true;
   this.strings = document.getElementById("downloadlogger-strings");
   this.dlMgr = Components.classes["@mozilla.org/download-manager;1"]
                          .getService(Components.interfaces.nsIDownloadManager);
   this.dlMgr.addListener(downloadlogger);
   // Open the database, placing its file in the profile directory
   this.dbFile = Components.classes["@mozilla.org/file/directory_service;1"]
                    .getService(Components.interfaces.nsIProperties)
                    .get("ProfD", Components.interfaces.nsIFile);
   this.dbFile.append("downloadlogger.sqlite");
   // Get access to the storage service and open the database
   this.storageService = Components.classes["@mozilla.org/storage/service;1"]
                       .getService(Components.interfaces.mozIStorageService);
   var dbConn = this.storageService.openDatabase(this.dbFile);
   // Now create the table; if it already exists, this fails, but we don't care!
   dbConn.executeSimpleSQL("CREATE TABLE items (source TEXT, size INTEGER," +
                           " startTime INTEGER, endTime INTEGER," +
                           " speed REAL, status INTEGER)");
   dbConn.close();
 },

This is fairly simple stuff. The Download Manager instance is cached into a member variable in the downloadlogger object for reuse later, and its addListener() method is called to start listening for download status updates. The database file is opened, and an sqlite CREATE TABLE command is executed to create the table.

Finally, the database is closed.

Note: The mozIStorageConnection method close() is being added to Firefox 3 alpha 8; in prior versions of Firefox, there is no way to explicitly close the database. Instead, it is closed when the garbage collector disposes of the connection object.

Handling download state changes

Once the code above is run, our onDownloadStateChange() method is called whenever a download's state changes. This is part of the nsIDownloadProgressListener interface.

That code looks like this:

 onDownloadStateChange: function(aState, aDownload) {
   var statement;
   switch(aDownload.state) {
     case Components.interfaces.nsIDownloadManager.DOWNLOAD_DOWNLOADING:
       // Add a new row for the download being started; each row includes the
       // source URI, size, and start time.  The end time and download speed
       // are both set to 0 at first, since we don't know those yet.
       // status is the same status value that came from the download manager.
       var dbConn = this.storageService.openDatabase(this.dbFile);
       statement = dbConn.createStatement("REPLACE INTO items VALUES " +
                                          "(?1, ?2, ?3, 0, 0.0, 0)");
       statement.bindStringParameter(0, aDownload.source.spec);
       statement.bindInt64Parameter(1, aDownload.size);
       statement.bindInt64Parameter(2, aDownload.startTime);
       statement.execute();
       statement.reset();
       dbConn.close();
       break;
     // Record the completion (whether failed or successful) of the download
     case Components.interfaces.nsIDownloadManager.DOWNLOAD_FINISHED:
     case Components.interfaces.nsIDownloadManager.DOWNLOAD_FAILED:
     case Components.interfaces.nsIDownloadManager.DOWNLOAD_CANCELED:
       this.logTransferCompleted(aDownload);
       break;
   }
 },

We're interested in four states. If the download's state, indicated by the aDownload.state field, is Components.interfaces.nsIDownloadManager.DOWNLOAD_DOWNLOADING, the file has begun to download. The aDownload object is an nsIDownload object.

In that case, we create a new row in our database for the new file by opening the database and building a REPLACE INTO sqlite command. The first three rows are set to the values of the source URI, file size, and start time fields from the download object. The remaining rows are set to zeroes since that's not information we have at the moment.

If the download's state indicates that the download is finished, canceled, or failed, we call our logTransferCompleted routine to update the log to indicate that state change. That code looks like this:

 logTransferCompleted: function(aDownload) {
     var endTime = new Date();                // Current time is the end time
     // Issue the REPLACE sqlite command to update the record.  We find a
     // record for the same source URI and start time, then update the end
     // time, size, and speed entries in the record.  By matching on both
     // source URI and start time, we support logging multiple downloads of
     // the same file.
     var dbConn = this.storageService.openDatabase(this.dbFile);
     var statement = dbConn.createStatement("UPDATE items SET size=?1, " + 
         "endTime=?2, speed=?3, status=?4 WHERE source=?5 and startTime=?6");
     statement.bindInt64Parameter(0, aDownload.size);
     statement.bindInt64Parameter(1, endTime.getTime());
     statement.bindDoubleParameter(2, aDownload.speed);
     statement.bindInt32Parameter(3, aDownload.state);
     statement.bindStringParameter(4, aDownload.source.spec);
     statement.bindInt64Parameter(5, aDownload.startTime);
     statement.execute();
     statement.reset();
     dbConn.close();
 },

This simply opens the database and builds and executes a UPDATE sqlite command that finds the download item whose source URI and start time match the download that has completed and updates its information. By looking for a record with both the same URI and start time, we properly support the case where the user downloads the same file multiple times.

Displaying the download log

The download log window's code is encapsulated in an object called downloadlogger_dlwindow. Since this is a simple example, it's a one-shot log window; it doesn't monitor for further changes to the log. It simply displays the state of downloads at the moment the window was opened.

That means all its work can be done in its load event handler, which looks like this:

 onLoad: function() {    
   // Open the database
   this.dbFile = Components.classes["@mozilla.org/file/directory_service;1"]
                    .getService(Components.interfaces.nsIProperties)
                    .get("ProfD", Components.interfaces.nsIFile);
   this.dbFile.append("downloadlogger.sqlite");
   // Get access to the storage service and open the database
   this.storageService = Components.classes["@mozilla.org/storage/service;1"]
                       .getService(Components.interfaces.mozIStorageService);
   var dbConn = this.storageService.openDatabase(this.dbFile);
   var loglist = document.getElementById("loglist");
   var statement = dbConn.createStatement("SELECT * FROM items");   // Get all items in table
   try {
     while (statement.executeStep()) {
       var row = document.createElement('listitem');
       // Add the cells to the row
       var cell = document.createElement('listcell');
       var sourceStr = statement.getString(0);
       row.setAttribute("tooltiptext", sourceStr);
       sourceStr = sourceStr.slice(sourceStr.lastIndexOf("/")+1);
       cell.setAttribute("label", sourceStr);   // Source
       row.appendChild(cell);
       cell = document.createElement('listcell');
       cell.setAttribute("label", (statement.getInt64(1) / 1024).toFixed(1) + "KB");    // Size
       cell.setAttribute("style", "text-align:right");
       row.appendChild(cell);
       var theDate = new Date(statement.getInt64(2) / 1000);        // Start time
       cell = document.createElement('listcell');
       var dateStr = theDate.toLocaleString();
       cell.setAttribute("label", dateStr);
       row.appendChild(cell);
       theDate = new Date(statement.getInt64(3));            // End time
       cell = document.createElement('listcell');
       dateStr = theDate.toLocaleString();
       cell.setAttribute("label", dateStr);
       row.appendChild(cell);
       var speed = statement.getDouble(4) / 1024.0;
       cell = document.createElement('listcell');
       cell.setAttribute("label", speed.toFixed(1) + "KB/sec");
       cell.setAttribute("style", "text-align:right");
       row.appendChild(cell);
       var status = statement.getInt32(5);
       var style = "color:black";
       cell = document.createElement('listcell');
       var statusStr;
       switch(status) {
         case 0:
           statusStr = "Downloading";
           break;
         case 1:
           statusStr = "Complete";
           style = "color:green";
           break;
         case 2:
           statusStr = "Failed";
           style = "color:red";
           break;
         case 3:
           statusStr = "Canceled";
           style = "color:purple";
           break;
         case 4:
           statusStr = "Paused";
           style = "color:blue";
           break;
         case 5:
           statusStr = "Queued";
           style = "color:teal";
           break;
         case 6:
           statusStr = "Blocked";
           style = "color:white background-color:red";
           break;
         case 7:
           statusStr = "Scanning";
           style = "color:silver";
           break;
         default:
           statusStr = "Unknown";
           break;
       }
       cell.setAttribute("label", statusStr);
       cell.setAttribute("style", style);
       row.appendChild(cell);
       loglist.appendChild(row);
     }
   } finally {
     statement.reset();
     dbConn = null;
   }
 }

This code is fairly simple. It starts by opening the sqlite database containing the log information, then creates a SELECT SQL statement to pull all entries from the database.

To iterate over the results, we use a while loop that calls the mozIStorageStatement object's executeStep() method. Each time that method is called, one row of the results is fetched.

After that, the list row object is created, and each entry in the search result is fetched and inserted into the appropriate list cell.

The interesting bits to take away from this:

  • mozIStorageStatement has several data getter routines for fetching search results, including getString(), getDouble(), and getInt64(). These methods take as a parameter the zero-based index number of the column whose value you wish to retrieve.
  • Note that the start time is being divided by 1000 before we create a JavaScript Date object from it. That's to adjust the value from the granularity stored in the database to that expected by JavaScript.
  • To right-justify the numeric columns, we set the appropriate cells' style attribute to text-align:right.

Exercises for the reader

There are some obvious things that could be done to improve this extension. If you're learning to use the Download Manager or Storage APIs, they're things you might look into doing for practice:

  • Add code to update the download log window on the fly, instead of generating a static list when it's first opened.
  • Add additional statistics. What's the average download speed across all downloads? What times of day do you get the best download performance?
  • Add buttons to delete items from the log, or to delete all items that have finished downloading.
  • Add searching.

See also

Storage, nsIDownloadManager, nsIDownload, nsIDownloadProgressListener

Document Tags and Contributors

 Contributors to this page: teoli, kmaglione, newacct, Mgjbot, Kohei, Nickolay, Sheppy
 Last updated by: kmaglione,