Bug 591289 - Save chosen download file name and other metadata in Places history [r=sdwilsh, ui-r=limi]
authorPaolo Amadini <paolo.mozmail@amadzone.org>
Mon, 15 Aug 2011 18:08:48 -0700
changeset 75343 213b48aa2c56a0dc6e570e60e76aa5afcdb31031
parent 75342 4c6dfeb5dc3ab1d9015f0b38441ed69251b3d345
child 75344 26114d0e96091d3b142be393f98a53ec10ad7be9
push idunknown
push userunknown
push dateunknown
reviewerssdwilsh, limi
bugs591289
milestone8.0a1
Bug 591289 - Save chosen download file name and other metadata in Places history [r=sdwilsh, ui-r=limi]
docshell/base/nsDownloadHistory.cpp
docshell/base/nsIDownloadHistory.idl
toolkit/components/downloads/nsDownloadManager.cpp
toolkit/components/places/nsNavHistory.cpp
toolkit/components/places/tests/head_common.js
toolkit/components/places/tests/unit/test_download_history.js
uriloader/exthandler/nsExternalHelperAppService.cpp
--- a/docshell/base/nsDownloadHistory.cpp
+++ b/docshell/base/nsDownloadHistory.cpp
@@ -53,17 +53,18 @@
 NS_IMPL_ISUPPORTS1(nsDownloadHistory, nsIDownloadHistory)
 
 ////////////////////////////////////////////////////////////////////////////////
 //// nsIDownloadHistory
 
 NS_IMETHODIMP
 nsDownloadHistory::AddDownload(nsIURI *aSource,
                                nsIURI *aReferrer,
-                               PRTime aStartTime)
+                               PRTime aStartTime,
+                               nsIURI *aDestination)
 {
   NS_ENSURE_ARG_POINTER(aSource);
 
   nsCOMPtr<nsIGlobalHistory2> history =
     do_GetService("@mozilla.org/browser/global-history;2");
   if (!history)
     return NS_ERROR_NOT_AVAILABLE;
 
--- a/docshell/base/nsIDownloadHistory.idl
+++ b/docshell/base/nsIDownloadHistory.idl
@@ -41,32 +41,36 @@
 
 interface nsIURI;
 
 /**
  * This interface can be used to add a download to history.  There is a separate
  * interface specifically for downloads in case embedders choose to track
  * downloads differently from other types of history.
  */
-[scriptable, uuid(202533cd-a8f1-4ee4-8d20-3a6a0d2c6c51)]
+[scriptable, uuid(a7a3358c-9af2-41e3-adfe-3bf0b7ac2c38)]
 interface nsIDownloadHistory : nsISupports {
   /**
    * Adds a download to history.  This will also notify observers that the
    * URI aSource is visited with the topic NS_LINK_VISITED_EVENT_TOPIC if
    * aSource has not yet been visited.
    *
    * @param aSource
    *        The source of the download we are adding to history.  This cannot be
    *        null.
    * @param aReferrer
    *        [optional] The referrer of source URI.
    * @param aStartTime
    *        [optional] The time the download was started.  If the start time
    *        is not given, the current time is used.
+   * @param aDestination
+   *        [optional] The target where the download is to be saved on the local
+   *        filesystem.
    * @throws NS_ERROR_NOT_AVAILABLE
    *         In a situation where a history implementation is not available,
    *         where 'history implementation' refers to something like
    *         nsIGlobalHistory and friends.
    */
   void addDownload(in nsIURI aSource, [optional] in nsIURI aReferrer,
-                   [optional] in PRTime aStartTime);
+                   [optional] in PRTime aStartTime,
+                   [optional] in nsIURI aDestination);
 };
 
--- a/toolkit/components/downloads/nsDownloadManager.cpp
+++ b/toolkit/components/downloads/nsDownloadManager.cpp
@@ -2382,17 +2382,17 @@ nsDownload::OnProgressChange64(nsIWebPro
       mReferrer = referrer;
 
     // If we have a MIME info, we know that exthandler has already added this to
     // the history, but if we do not, we'll have to add it ourselves.
     if (!mMIMEInfo) {
       nsCOMPtr<nsIDownloadHistory> dh =
         do_GetService(NS_DOWNLOADHISTORY_CONTRACTID);
       if (dh)
-        (void)dh->AddDownload(mSource, mReferrer, mStartTime);
+        (void)dh->AddDownload(mSource, mReferrer, mStartTime, mTarget);
     }
 
     // Fetch the entityID, but if we can't get it, don't panic (non-resumable)
     nsCOMPtr<nsIResumableChannel> resumableChannel(do_QueryInterface(aRequest));
     if (resumableChannel)
       (void)resumableChannel->GetEntityID(mEntityID);
 
     // Update the state and the database
--- a/toolkit/components/places/nsNavHistory.cpp
+++ b/toolkit/components/places/nsNavHistory.cpp
@@ -24,16 +24,17 @@
  *   Seth Spitzer <sspitzer@mozilla.com>
  *   Asaf Romano <mano@mozilla.com>
  *   Marco Bonardo <mak77@bonardo.net>
  *   Edward Lee <edward.lee@engineering.uiuc.edu>
  *   Michael Ventnor <m.ventnor@gmail.com>
  *   Ehsan Akhgari <ehsan.akhgari@gmail.com>
  *   Drew Willcoxon <adw@mozilla.com>
  *   Philipp von Weitershausen <philipp@weitershausen.de>
+ *   Paolo Amadini <http://www.amadzone.org/>
  *
  * 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
@@ -165,16 +166,24 @@ using namespace mozilla::places;
 static const PRInt64 USECS_PER_DAY = LL_INIT(20, 500654080);
 
 // character-set annotation
 #define CHARSET_ANNO NS_LITERAL_CSTRING("URIProperties/characterSet")
 
 // Sync guid annotation
 #define SYNCGUID_ANNO NS_LITERAL_CSTRING("sync/guid")
 
+// Download destination file URI annotation
+#define DESTINATIONFILEURI_ANNO \
+  NS_LITERAL_CSTRING("downloads/destinationFileURI")
+
+// Download destination file name annotation
+#define DESTINATIONFILENAME_ANNO \
+  NS_LITERAL_CSTRING("downloads/destinationFileName")
+
 // These macros are used when splitting history by date.
 // These are the day containers and catch-all final container.
 #define HISTORY_ADDITIONAL_DATE_CONT_NUM 3
 // We use a guess of the number of months considering all of them 30 days
 // long, but we split only the last 6 months.
 #define HISTORY_DATE_CONT_NUM(_daysFromOldestVisit) \
   (HISTORY_ADDITIONAL_DATE_CONT_NUM + \
    NS_MIN(6, (PRInt32)ceilf((float)_daysFromOldestVisit/30)))
@@ -5211,28 +5220,82 @@ nsNavHistory::OnEndVacuum(PRBool aSuccee
 }
 
 
 ////////////////////////////////////////////////////////////////////////////////
 //// nsIDownloadHistory
 
 NS_IMETHODIMP
 nsNavHistory::AddDownload(nsIURI* aSource, nsIURI* aReferrer,
-                          PRTime aStartTime)
+                          PRTime aStartTime, nsIURI* aDestination)
 {
   NS_ASSERTION(NS_IsMainThread(), "This can only be called on the main thread");
   NS_ENSURE_ARG(aSource);
 
   // don't add when history is disabled and silently fail
   if (IsHistoryDisabled())
     return NS_OK;
 
   PRInt64 visitID;
-  return AddVisit(aSource, aStartTime, aReferrer, TRANSITION_DOWNLOAD, PR_FALSE,
-                  0, &visitID);
+  nsresult rv = AddVisit(aSource, aStartTime, aReferrer, TRANSITION_DOWNLOAD,
+                         PR_FALSE, 0, &visitID);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  if (!aDestination) {
+    return NS_OK;
+  }
+
+  // Exit silently if the download destination is not a local file.
+  nsCOMPtr<nsIFileURL> destinationFileURL = do_QueryInterface(aDestination);
+  if (!destinationFileURL) {
+    return NS_OK;
+  }
+
+  nsCOMPtr<nsIFile> destinationFile;
+  rv = destinationFileURL->GetFile(getter_AddRefs(destinationFile));
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsAutoString destinationFileName;
+  rv = destinationFile->GetLeafName(destinationFileName);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  nsCAutoString destinationURISpec;
+  rv = destinationFileURL->GetSpec(destinationURISpec);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  // Use annotations for storing the additional download metadata.
+  nsAnnotationService* annosvc = nsAnnotationService::GetAnnotationService();
+  NS_ENSURE_TRUE(annosvc, NS_ERROR_OUT_OF_MEMORY);
+
+  (void)annosvc->SetPageAnnotationString(
+    aSource,
+    DESTINATIONFILEURI_ANNO,
+    NS_ConvertUTF8toUTF16(destinationURISpec),
+    0,
+    nsIAnnotationService::EXPIRE_WITH_HISTORY
+  );
+
+  (void)annosvc->SetPageAnnotationString(
+    aSource,
+    DESTINATIONFILENAME_ANNO,
+    destinationFileName,
+    0,
+    nsIAnnotationService::EXPIRE_WITH_HISTORY
+  );
+
+  // In case we are downloading a file that does not correspond to a web
+  // page for which the title is present, we populate the otherwise empty
+  // history title with the name of the destination file, to allow it to be
+  // visible and searchable in history results.
+  nsAutoString title;
+  if (NS_SUCCEEDED(GetPageTitle(aSource, title)) && title.IsEmpty()) {
+    (void)SetPageTitle(aSource, destinationFileName);
+  }
+
+  return NS_OK;
 }
 
 
 ////////////////////////////////////////////////////////////////////////////////
 //// nsPIPlacesDatabase
 
 NS_IMETHODIMP
 nsNavHistory::GetDBConnection(mozIStorageConnection **_DBConnection)
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -60,16 +60,21 @@ XPCOMUtils.defineLazyGetter(this, "Servi
   return Services;
 });
 
 XPCOMUtils.defineLazyGetter(this, "NetUtil", function() {
   Cu.import("resource://gre/modules/NetUtil.jsm");
   return NetUtil;
 });
 
+XPCOMUtils.defineLazyGetter(this, "FileUtils", function() {
+  Cu.import("resource://gre/modules/FileUtils.jsm");
+  return FileUtils;
+});
+
 XPCOMUtils.defineLazyGetter(this, "PlacesUtils", function() {
   Cu.import("resource://gre/modules/PlacesUtils.jsm");
   return PlacesUtils;
 });
 
 
 function LOG(aMsg) {
   aMsg = ("*** PLACES TESTS: " + aMsg);
--- a/toolkit/components/places/tests/unit/test_download_history.js
+++ b/toolkit/components/places/tests/unit/test_download_history.js
@@ -1,47 +1,15 @@
-/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
-/* vim:set ts=2 sw=2 sts=2 et: */
-/* ***** 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 mozilla.org code.
- *
- * The Initial Developer of the Original Code is
- * Mozilla Corporation.
- * Portions created by the Initial Developer are Copyright (C) 2007
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *   Shawn Wilsher <me@shawnwilsher.com> (Original Author)
- *   Marco Bonardo <mak77@bonardo.net>
- *
- * 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 ***** */
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * The first part of this file runs a series of tests for the synchronous
+ * behavior of the nsIDownloadHistory::AddDownload function.
+ */
 
 // Get services
 var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
               getService(Ci.nsINavHistoryService);
 let bh = histsvc.QueryInterface(Ci.nsIBrowserHistory);
 var os = Cc["@mozilla.org/observer-service;1"].
          getService(Ci.nsIObserverService);
 var prefs = Cc["@mozilla.org/preferences-service;1"].
@@ -154,9 +122,98 @@ function run_test() {
     (tests.shift())();
 
     // Cleanup
     bh.removeAllPages();
     observer.topicReceived = false;
   }
 
   os.removeObserver(observer, NS_LINK_VISITED_EVENT_TOPIC);
+
+  // Asynchronous part of the test.
+  test_dh_details();
 }
+
+/**
+ * The second part of this file tests that nsIDownloadHistory::AddDownload saves
+ * the additional download details if the optional destination URL is specified.
+ */
+
+function test_dh_details()
+{
+  do_test_pending();
+
+  const SOURCE_URI = uri("http://example.com/test_download_history_details");
+  const DEST_FILE_NAME = "dest.txt";
+
+  // We must build a real, valid file URI for the destination.
+  let destFileUri = NetUtil.newURI(FileUtils.getFile("TmpD", [DEST_FILE_NAME]));
+
+  let titleSet = false;
+  let destinationFileUriSet = false;
+  let destinationFileNameSet = false;
+
+  function checkFinished()
+  {
+    if (titleSet && destinationFileUriSet && destinationFileNameSet) {
+      PlacesUtils.annotations.removeObserver(annoObserver);
+      PlacesUtils.history.removeObserver(historyObserver);
+
+      // Cleanup.
+      bh.removeAllPages();
+      do_test_finished();
+    }
+  };
+
+  let annoObserver = {
+    onPageAnnotationSet: function AO_onPageAnnotationSet(aPage, aName)
+    {
+      if (aPage.equals(SOURCE_URI)) {
+        let value = PlacesUtils.annotations.getPageAnnotation(aPage, aName);
+        switch (aName)
+        {
+          case "downloads/destinationFileURI":
+            destinationFileUriSet = true;
+            do_check_eq(value, destFileUri.spec);
+            break;
+          case "downloads/destinationFileName":
+            destinationFileNameSet = true;
+            do_check_eq(value, DEST_FILE_NAME);
+            break;
+        }
+        checkFinished();
+      }
+    },
+    onItemAnnotationSet: function() {},
+    onPageAnnotationRemoved: function() {},
+    onItemAnnotationRemoved: function() {}
+  }
+
+  let historyObserver = {
+    onBeginUpdateBatch: function() {},
+    onEndUpdateBatch: function() {},
+    onVisit: function() {},
+    onTitleChanged: function HO_onTitleChanged(aURI, aPageTitle)
+    {
+      if (aURI.equals(SOURCE_URI)) {
+        titleSet = true;
+        do_check_eq(aPageTitle, DEST_FILE_NAME);
+        checkFinished();
+      }
+    },
+    onBeforeDeleteURI: function() {},
+    onDeleteURI: function() {},
+    onClearHistory: function() {},
+    onPageChanged: function() {},
+    onDeleteVisits: function() {}   
+  };
+
+  PlacesUtils.annotations.addObserver(annoObserver, false);
+  PlacesUtils.history.addObserver(historyObserver, false);
+
+  // Both null values and remote URIs should not cause errors.
+  dh.addDownload(SOURCE_URI, null, Date.now() * 1000);
+  dh.addDownload(SOURCE_URI, null, Date.now() * 1000, null);
+  dh.addDownload(SOURCE_URI, null, Date.now() * 1000, uri("http://localhost/"));
+
+  // Valid local file URIs should cause the download details to be saved.
+  dh.addDownload(SOURCE_URI, null, Date.now() * 1000, destFileUri);
+}
--- a/uriloader/exthandler/nsExternalHelperAppService.cpp
+++ b/uriloader/exthandler/nsExternalHelperAppService.cpp
@@ -1772,17 +1772,21 @@ NS_IMETHODIMP nsExternalAppHandler::OnSt
   }
 
   // Now let's add the download to history
   nsCOMPtr<nsIDownloadHistory> dh(do_GetService(NS_DOWNLOADHISTORY_CONTRACTID));
   if (dh) {
     nsCOMPtr<nsIURI> referrer;
     if (aChannel)
       NS_GetReferrerFromChannel(aChannel, getter_AddRefs(referrer));
-    dh->AddDownload(mSourceUrl, referrer, mTimeDownloadStarted);
+
+    nsCOMPtr<nsIURI> target;
+    NS_NewFileURI(getter_AddRefs(target), mFinalFileDestination);
+
+    dh->AddDownload(mSourceUrl, referrer, mTimeDownloadStarted, target);
   }
 
   return NS_OK;
 }
 
 // Convert error info into proper message text and send OnStatusChange notification
 // to the web progress listener.
 void nsExternalAppHandler::SendStatusChange(ErrorType type, nsresult rv, nsIRequest *aRequest, const nsAFlatString &path)