Bug 419403 - Pimp My Download Manager Search! (multi word, match any text). r=sdwilsh, a1.9=beltzner
authoredward.lee@engineering.uiuc.edu
Tue, 26 Feb 2008 09:52:17 -0800
changeset 12270 d6350d079c6ab014fb39d36cea1ea2fd54e6725d
parent 12269 87661fe84265c75a834538f67e61a2732e50c95a
child 12271 dcee308be5884987bf653b2d38b45cc2962d0be6
push idunknown
push userunknown
push dateunknown
reviewerssdwilsh
bugs419403
milestone1.9b4pre
Bug 419403 - Pimp My Download Manager Search! (multi word, match any text). r=sdwilsh, a1.9=beltzner
toolkit/mozapps/downloads/content/downloads.js
toolkit/mozapps/downloads/tests/browser/Makefile.in
toolkit/mozapps/downloads/tests/browser/browser_multiword_search.js
--- a/toolkit/mozapps/downloads/content/downloads.js
+++ b/toolkit/mozapps/downloads/content/downloads.js
@@ -59,25 +59,32 @@ Cu.import("resource://gre/modules/Downlo
 Cu.import("resource://gre/modules/PluralForm.jsm");
 
 const nsIDM = Ci.nsIDownloadManager;
 
 let gDownloadManager = Cc["@mozilla.org/download-manager;1"].getService(nsIDM);
 let gDownloadListener = null;
 let gDownloadsView = null;
 let gSearchBox = null;
-let gSearchTerms = "";
+let gSearchTerms = [];
 let gBuilder = 0;
 
 // Control the performance of the incremental list building by setting how many
 // milliseconds to wait before building more of the list and how many items to
 // add between each delay.
 const gListBuildDelay = 300;
 const gListBuildChunk = 3;
 
+// Array of download richlistitem attributes to check when searching
+const gSearchAttributes = [
+  "target",
+  "status",
+  "dateTime",
+];
+
 // If the user has interacted with the window in a significant way, we should
 // not auto-close the window. Tough UI decisions about what is "significant."
 var gUserInteracted = false;
 
 // These strings will be converted to the corresponding ones from the string
 // bundle on startup.
 let gStr = {
   paused: "paused",
@@ -92,30 +99,21 @@ let gStr = {
   yesterday: "yesterday",
   monthDate: "monthDate",
   downloadsTitleFiles: "downloadsTitleFiles",
   downloadsTitlePercent: "downloadsTitlePercent",
   fileExecutableSecurityWarningTitle: "fileExecutableSecurityWarningTitle",
   fileExecutableSecurityWarningDontAsk: "fileExecutableSecurityWarningDontAsk"
 };
 
-// Create a getDisplayHost function for queries to use
-gDownloadManager.DBConnection.createFunction("getDisplayHost", 1, {
-  QueryInterface: XPCOMUtils.generateQI([Ci.mozIStorageFunction]),
-  onFunctionCall: function(aArgs)
-    DownloadUtils.getURIHost(aArgs.getUTF8String(0))[0]
-});
-
 // The statement to query for downloads that are active or match the search
 let gStmt = gDownloadManager.DBConnection.createStatement(
   "SELECT id, target, name, source, state, startTime, endTime, referrer, " +
-         "currBytes, maxBytes, state IN (?1, ?2, ?3, ?4, ?5) isActive, " +
-         "getDisplayHost(IFNULL(referrer, source)) display " +
+         "currBytes, maxBytes, state IN (?1, ?2, ?3, ?4, ?5) isActive " +
   "FROM moz_downloads " +
-  "WHERE isActive OR name LIKE ?6 ESCAPE '/' OR display LIKE ?6 ESCAPE '/' " +
   "ORDER BY isActive DESC, endTime DESC, startTime DESC");
 
 ////////////////////////////////////////////////////////////////////////////////
 //// Utility Functions
 
 function getDownload(aID)
 {
   return document.getElementById("dl" + aID);
@@ -133,19 +131,18 @@ function downloadCompleted(aDownload)
     let dl = getDownload(aDownload.id);
 
     // Update attributes now that we've finished
     dl.setAttribute("startTime", Math.round(aDownload.startTime / 1000));
     dl.setAttribute("endTime", Date.now());
     dl.setAttribute("currBytes", aDownload.amountTransferred);
     dl.setAttribute("maxBytes", aDownload.size);
 
-    // If we aren't displaying search results, move the download to after the
-    // active ones
-    if (gSearchTerms.length == 0) {
+    // Move the download below active if it should stay in the list
+    if (downloadMatchesSearch(dl)) {
       // Iterate down until we find a non-active download
       let next = dl.nextSibling;
       while (next && next.inProgress)
         next = next.nextSibling;
 
       // Move the item and color everything after where it moved from
       let fixup = dl.nextSibling;
       gDownloadsView.insertBefore(dl, next);
@@ -391,17 +388,17 @@ function Startup()
 
   // convert strings to those in the string bundle
   let (sb = document.getElementById("downloadStrings")) {
     let getStr = function(string) sb.getString(string);
     for (let [name, value] in Iterator(gStr))
       gStr[name] = typeof value == "string" ? getStr(value) : value.map(getStr);
   }
 
-  buildDownloadList();
+  buildDownloadList(true);
 
   // The DownloadProgressListener (DownloadProgressListener.js) handles progress
   // notifications.
   gDownloadListener = new DownloadProgressListener();
   gDownloadManager.addListener(gDownloadListener);
 
   // downloads can finish before Startup() does, so check if the window should
   // close and act accordingly
@@ -432,28 +429,26 @@ function Shutdown()
 
   let obs = Cc["@mozilla.org/observer-service;1"].
             getService(Ci.nsIObserverService);
   obs.removeObserver(gDownloadObserver, "download-manager-remove-download");
 
   clearTimeout(gBuilder);
   gStmt.reset();
   gStmt.finalize();
-
-  gDownloadManager.DBConnection.removeFunction("getDisplayHost");
 }
 
 let gDownloadObserver = {
   observe: function gdo_observe(aSubject, aTopic, aData) {
     switch (aTopic) {
       case "download-manager-remove-download":
         // A null subject here indicates "remove all"
         if (!aSubject) {
           // Rebuild the default view
-          buildDownloadList();
+          buildDownloadList(true);
           break;
         }
 
         // Otherwise, remove a single download
         let id = aSubject.QueryInterface(Ci.nsISupportsPRUint32);
         let dl = getDownload(id.data);
         removeFromView(dl);
         break;
@@ -1007,40 +1002,49 @@ function getReferrerOrSource(aDownload)
 
   // Otherwise, provide the source
   return aDownload.getAttribute("uri");
 }
 
 /**
  * Initiate building the download list to have the active downloads followed by
  * completed ones filtered by the search term if necessary.
+ *
+ * @param aForceBuild
+ *        Force the list to be built even if the search terms don't change
  */
-function buildDownloadList()
+function buildDownloadList(aForceBuild)
 {
+  // Stringify the previous search
+  let prevSearch = gSearchTerms ? gSearchTerms.join(" ") : null;
+
+  // Array of space-separated lower-case search terms
+  gSearchTerms = gSearchBox.value.replace(/^\s+|\s+$/g, "").
+                 toLowerCase().split(/\s+/);
+
+  // Unless forced, don't rebuild the download list if the search didn't change
+  if (!aForceBuild && gSearchTerms.join(" ") == prevSearch)
+    return;
+
   // Clear out values before using them
   clearTimeout(gBuilder);
   gStmt.reset();
 
   // Clear the list before adding items by replacing with a shallow copy
   let (empty = gDownloadsView.cloneNode(false)) {
     gDownloadsView.parentNode.replaceChild(empty, gDownloadsView);
     gDownloadsView = empty;
   }
 
-  gSearchTerms = gSearchBox.value.replace(/^\s+|\s+$/g, "");
-
-  let like = "%" + gStmt.escapeStringForLIKE(gSearchTerms, "/") + "%";
-
   try {
     gStmt.bindInt32Parameter(0, nsIDM.DOWNLOAD_NOTSTARTED);
     gStmt.bindInt32Parameter(1, nsIDM.DOWNLOAD_DOWNLOADING);
     gStmt.bindInt32Parameter(2, nsIDM.DOWNLOAD_PAUSED);
     gStmt.bindInt32Parameter(3, nsIDM.DOWNLOAD_QUEUED);
     gStmt.bindInt32Parameter(4, nsIDM.DOWNLOAD_SCANNING);
-    gStmt.bindStringParameter(5, like);
   } catch (e) {
     // Something must have gone wrong when binding, so clear and quit
     gStmt.reset();
     return;
   }
 
   // Take a quick break before we actually start building the list
   gBuilder = setTimeout(function() {
@@ -1079,29 +1083,34 @@ function stepListBuilder(aNumItems) {
 
     // Only add the referrer if it's not null
     let (referrer = gStmt.getString(7)) {
       if (referrer)
         attrs.referrer = referrer;
     }
 
     // If the download is active, grab the real progress, otherwise default 100
-    attrs.progress = gStmt.getInt32(10) ?
-      gDownloadManager.getDownload(attrs.dlid).percentComplete : 100;
+    let isActive = gStmt.getInt32(10);
+    attrs.progress = isActive ? gDownloadManager.getDownload(attrs.dlid).
+      percentComplete : 100;
 
-    // Make the item and add it to the end
+    // Make the item and add it to the end if it's active or matches the search
     let item = createDownloadItem(attrs);
-    if (item) {
+    if (item && (isActive || downloadMatchesSearch(item))) {
       // Add item to the end and color just that one item
       gDownloadsView.appendChild(item);
       stripeifyList(item);
     
       // Because of the joys of XBL, we can't update the buttons until the
       // download object is in the document.
       updateButtons(item);
+    } else {
+      // We didn't add an item, so bump up the number of items to process, but
+      // not a whole number so that we eventually do pause for a chunk break
+      aNumItems += .9;
     }
   } catch (e) {
     // Something went wrong when stepping or getting values, so clear and quit
     gStmt.reset();
     return;
   }
 
   // Add another item to the list if we should; otherwise, let the UI update
@@ -1145,16 +1154,41 @@ function prependList(aDownload)
     
     // Because of the joys of XBL, we can't update the buttons until the
     // download object is in the document.
     updateButtons(item);
   }
 }
 
 /**
+ * Check if the download matches the current search term based on the texts
+ * shown to the user. All search terms are checked to see if each matches any
+ * of the displayed texts.
+ *
+ * @param aItem
+ *        Download richlistitem to check if it matches the current search
+ * @return Boolean true if it matches the search; false otherwise
+ */
+function downloadMatchesSearch(aItem)
+{
+  // Search through the download attributes that are shown to the user and
+  // make it into one big string for easy combined searching
+  let combinedSearch = "";
+  for each (let attr in gSearchAttributes)
+    combinedSearch += aItem.getAttribute(attr).toLowerCase() + " ";
+
+  // Make sure each of the terms are found
+  for each (let term in gSearchTerms)
+    if (combinedSearch.search(term) == -1)
+      return false;
+
+  return true;
+}
+
+/**
  * Stripeify the download list by setting or clearing the "alternate" attribute
  * on items starting from a particular item and continuing to the end.
  *
  * @param aItem
  *        Download rishlist item to start stripeifying
  */
 function stripeifyList(aItem)
 {
--- a/toolkit/mozapps/downloads/tests/browser/Makefile.in
+++ b/toolkit/mozapps/downloads/tests/browser/Makefile.in
@@ -48,16 +48,17 @@ include $(topsrcdir)/config/rules.mk
 _BROWSER_FILES = \
   browser_basic_functionality.js \
   browser_bug_411172.js \
   browser_bug_394039.js \
   browser_bug_410289.js \
   browser_bug_413985.js \
   browser_bug_416303.js \
   browser_bug_406857.js \
+  browser_multiword_search.js \
   $(NULL)
 
 ifneq (,$(filter cocoa, $(MOZ_WIDGET_TOOLKIT)))
 _BROWSER_FILES += \
   browser_bug_411172_mac.js \
   $(NULL)
 endif
 
new file mode 100644
--- /dev/null
+++ b/toolkit/mozapps/downloads/tests/browser/browser_multiword_search.js
@@ -0,0 +1,143 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Download Manager UI Test Code.
+ *
+ * The Initial Developer of the Original Code is
+ * Edward Lee <edward.lee@engineering.uiuc.edu>.
+ * Portions created by the Initial Developer are Copyright (C) 2008
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Test for bug 419403 that lets the download manager support multiple word
+ * search against the download name, source/referrer, date/time, file size,
+ * etc.
+ */
+
+function test()
+{
+  let dm = Cc["@mozilla.org/download-manager;1"].
+           getService(Ci.nsIDownloadManager);
+  let db = dm.DBConnection;
+
+  // Empty any old downloads
+  db.executeSimpleSQL("DELETE FROM moz_downloads");
+
+  let stmt = db.createStatement(
+    "INSERT INTO moz_downloads (name, target, source, state, endTime, maxBytes) " +
+    "VALUES (?1, ?2, ?3, ?4, ?5, ?6)");
+
+  for each (let site in ["ed.agadak.net", "mozilla.org"]) {
+    stmt.bindStringParameter(0, "Super Pimped Download");
+    stmt.bindStringParameter(1, "file://dummy/file");
+    stmt.bindStringParameter(2, "http://" + site + "/file");
+    stmt.bindInt32Parameter(3, dm.DOWNLOAD_FINISHED);
+    stmt.bindInt64Parameter(4, new Date(1985, 7, 2) * 1000);
+    stmt.bindInt64Parameter(5, 111222333444);
+
+    // Add it!
+    stmt.execute();
+  }
+
+  stmt.finalize();
+
+  // Close the UI if necessary
+  let wm = Cc["@mozilla.org/appshell/window-mediator;1"].
+           getService(Ci.nsIWindowMediator);
+  let win = wm.getMostRecentWindow("Download:Manager");
+  if (win) win.close();
+
+  // Start the test when the download manager window loads
+  let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
+           getService(Ci.nsIWindowWatcher);
+  ww.registerNotification({
+    observe: function(aSubject, aTopic, aData) {
+      ww.unregisterNotification(this);
+      aSubject.QueryInterface(Ci.nsIDOMEventTarget).
+      addEventListener("DOMContentLoaded", doTest, false);
+    }
+  });
+
+  let testPhase = 0;
+
+  // Let the Startup method of the download manager UI finish before we test
+  let doTest = function() setTimeout(function() {
+    win = wm.getMostRecentWindow("Download:Manager");
+    let $ = function(id) win.document.getElementById(id);
+
+    let downloadView = $("downloadView");
+    let searchbox = $("searchbox");
+
+    // Try again if selectedIndex is -1
+    if (downloadView.selectedIndex)
+      return doTest();
+
+    // The list must have built, so figure out what test to do
+    switch (testPhase) {
+      case 0:
+        // Search for multiple words in any order in all places
+        searchbox.value = "download super pimped 104 GB august 2";
+        searchbox.doCommand();
+
+        // Next phase eventually checks for two downloads
+        testPhase++;
+        return doTest();
+      case 1:
+        // List is being populated
+        ok(downloadView.itemCount > 0, "Search found something");
+
+        // Actually check for the two downloads
+        testPhase++;
+        return doTest();
+      case 2:
+        // Done populating the two items
+        ok(downloadView.itemCount == 2, "Search matched both downloads");
+
+        // Do partial word matches including the site
+        searchbox.value = "agadak aug 2 downl 104";
+        searchbox.doCommand();
+
+        // Next phase checks for a single item
+        testPhase++;
+        return doTest();
+      case 3:
+        // Done populating the one result
+        ok(downloadView.itemCount == 1, "Found the single download");
+
+        // We're done!
+        return finish();
+    }
+  }, 0);
+ 
+  // Show the Download Manager UI
+  Cc["@mozilla.org/download-manager-ui;1"].
+  getService(Ci.nsIDownloadManagerUI).show();
+
+  waitForExplicitFinish();
+}