Bug 825849 - Add a RemoveAllDownloads API to nsIDownloadHistory.
authorMarco Bonardo <mbonardo@mozilla.com>
Sat, 05 Jan 2013 10:21:04 +0100
changeset 117726 9b891394bb82f14f55665fb36606e2ae810dc4f7
parent 117725 08570379483ee9e80a8807bab27ee831e37a7c7a
child 117727 8101b0c28d50448d9f647e115645115a23cce65a
push idunknown
push userunknown
push dateunknown
bugs825849
milestone20.0a1
Bug 825849 - Add a RemoveAllDownloads API to nsIDownloadHistory. r=Mano sr=gavin
browser/components/downloads/content/allDownloadsViewOverlay.js
browser/components/downloads/content/allDownloadsViewOverlay.xul
browser/locales/en-US/chrome/browser/downloads/downloads.dtd
docshell/base/nsDownloadHistory.cpp
docshell/base/nsIDownloadHistory.idl
toolkit/components/downloads/nsDownloadManager.cpp
toolkit/components/places/History.cpp
toolkit/components/places/History.h
toolkit/components/places/nsINavHistoryService.idl
toolkit/components/places/nsNavBookmarks.cpp
toolkit/components/places/nsNavHistory.cpp
toolkit/components/places/nsNavHistoryResult.cpp
toolkit/components/places/nsNavHistoryResult.h
toolkit/components/places/nsPIPlacesHistoryListenersNotifier.idl
toolkit/components/places/nsPlacesExpiration.js
toolkit/components/places/tests/head_common.js
toolkit/components/places/tests/unit/test_download_history.js
--- a/browser/components/downloads/content/allDownloadsViewOverlay.js
+++ b/browser/components/downloads/content/allDownloadsViewOverlay.js
@@ -7,33 +7,37 @@
  * IT IS HIGHLY RECOMMENDED NOT TO EXTEND IT FOR ANY OTHER USE CASES OR RELY
  * ON IT AS AN API.
  */
 
 let Cu = Components.utils;
 let Ci = Components.interfaces;
 let Cc = Components.classes;
 
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/DownloadUtils.jsm");
 Cu.import("resource:///modules/DownloadsCommon.jsm");
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 
+XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
+                                  "resource://gre/modules/PrivateBrowsingUtils.jsm");
+
 const nsIDM = Ci.nsIDownloadManager;
 
 const DESTINATION_FILE_URI_ANNO  = "downloads/destinationFileURI";
 const DESTINATION_FILE_NAME_ANNO = "downloads/destinationFileName";
 const DOWNLOAD_STATE_ANNO        = "downloads/state";
 
 const DOWNLOAD_VIEW_SUPPORTED_COMMANDS =
  ["cmd_delete", "cmd_copy", "cmd_paste", "cmd_selectAll",
   "downloadsCmd_pauseResume", "downloadsCmd_cancel",
   "downloadsCmd_open", "downloadsCmd_show", "downloadsCmd_retry",
-  "downloadsCmd_openReferrer"];
+  "downloadsCmd_openReferrer", "downloadsCmd_clearDownloads"];
 
 const NOT_AVAILABLE = Number.MAX_VALUE;
 
 function GetFileForFileURI(aFileURI)
   Cc["@mozilla.org/network/protocol;1?name=file"]
     .getService(Ci.nsIFileProtocolHandler)
     .getFileFromURLSpec(aFileURI);
 
@@ -821,17 +825,17 @@ DownloadsPlacesView.prototype = {
     else {
       shell.dataItem = null;
       // Move it below the session-download items;
       if (this._lastSessionDownloadElement == shell.dataItem) {
         this._lastSessionDownloadElement = shell.dataItem.previousSibling;
       }
       else {
         let before = this._lastSessionDownloadElement ?
-          this._lastSessionDownloadElement.nextSibling : this._richlistbox.firstchild;
+          this._lastSessionDownloadElement.nextSibling : this._richlistbox.firstChild;
         this._richlistbox.insertBefore(shell.element, before);
       }
     }
   },
 
   _place: "",
   get place() this._place,
   set place(val) {
@@ -960,16 +964,17 @@ DownloadsPlacesView.prototype = {
   nodeDateAddedChanged: function() {},
   nodeLastModifiedChanged: function() {},
   nodeReplaced: function() {},
   nodeHistoryDetailsChanged: function() {},
   nodeTagsChanged: function() {},
   sortingChanged: function() {},
   nodeMoved: function() {},
   nodeURIChanged: function() {},
+  batching: function() {},
 
   get controller() this._richlistbox.controller,
 
   get searchTerm() this._searchTerm,
   set searchTerm(aValue) {
     if (this._searchTerm != aValue) {
       for (let element of this._richlistbox.childNodes) {
         element.hidden = !element._shell.matchesSearchTerm(aValue);
@@ -1007,16 +1012,18 @@ DownloadsPlacesView.prototype = {
     let selectedElements = this._richlistbox.selectedItems;
     switch (aCommand) {
       case "cmd_copy":
         return selectedElements && selectedElements.length > 0;
       case "cmd_selectAll":
         return true;
       case "cmd_paste":
         return this._canDownloadClipboardURL();
+      case "downloadsCmd_clearDownloads":
+        return !!this._richlistbox.firstChild;
       default:
         return Array.every(selectedElements, function(element) {
           return element._shell.isCommandEnabled(aCommand);
         });
     }
   },
 
   _copySelectedDownloadsToClipboard:
@@ -1068,16 +1075,28 @@ DownloadsPlacesView.prototype = {
         this._copySelectedDownloadsToClipboard();
         break;
       case "cmd_selectAll":
         this._richlistbox.selectAll();
         break;
       case "cmd_paste":
         this._downloadURLFromClipboard();
         break;
+      case "downloadsCmd_clearDownloads":
+        if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+          Services.downloads.cleanUpPrivate();
+        } else {
+          Services.downloads.cleanUp();
+        }
+        if (this.result) {
+          Cc["@mozilla.org/browser/download-history;1"]
+            .getService(Ci.nsIDownloadHistory)
+            .removeAllDownloads();
+        }
+        break;
       default: {
         let selectedElements = this._richlistbox.selectedItems;
         for (let element of selectedElements) {
           element._shell.doCommand(aCommand);
         }
       }
     }
   },
--- a/browser/components/downloads/content/allDownloadsViewOverlay.xul
+++ b/browser/components/downloads/content/allDownloadsViewOverlay.xul
@@ -59,16 +59,18 @@
     <command id="downloadsCmd_open"
              oncommand="goDoCommand('downloadsCmd_open')"/>
     <command id="downloadsCmd_show"
              oncommand="goDoCommand('downloadsCmd_show')"/>
     <command id="downloadsCmd_retry"
              oncommand="goDoCommand('downloadsCmd_retry')"/>
     <command id="downloadsCmd_openReferrer"
              oncommand="goDoCommand('downloadsCmd_openReferrer')"/>
+    <command id="downloadsCmd_clearDownloads"
+             oncommand="goDoCommand('downloadsCmd_clearDownloads')"/>
   </commandset>
 
   <menupopup id="downloadsContextMenu" class="download-state">
     <menuitem command="downloadsCmd_pauseResume"
               class="downloadPauseMenuItem"
               label="&cmd.pause.label;"
               accesskey="&cmd.pause.accesskey;"/>
     <menuitem command="downloadsCmd_pauseResume"
@@ -97,10 +99,16 @@
     <menuseparator class="downloadCommandsSeparator"/>
 
     <menuitem command="downloadsCmd_openReferrer"
               label="&cmd.goToDownloadPage.label;"
               accesskey="&cmd.goToDownloadPage.accesskey;"/>
     <menuitem command="cmd_copy"
               label="&cmd.copyDownloadLink.label;"
               accesskey="&cmd.copyDownloadLink.accesskey;"/>
+
+    <menuseparator/>
+
+    <menuitem command="downloadsCmd_clearDownloads"
+              label="&cmd.clearDownloads.label;"
+              accesskey="&cmd.clearDownloads.accesskey;"/>
   </menupopup>
 </overlay>
--- a/browser/locales/en-US/chrome/browser/downloads/downloads.dtd
+++ b/browser/locales/en-US/chrome/browser/downloads/downloads.dtd
@@ -59,16 +59,18 @@
 <!ENTITY cmd.goToDownloadPage.label       "Go To Download Page">
 <!ENTITY cmd.goToDownloadPage.accesskey   "G">
 <!ENTITY cmd.copyDownloadLink.label       "Copy Download Link">
 <!ENTITY cmd.copyDownloadLink.accesskey   "L">
 <!ENTITY cmd.removeFromHistory.label      "Remove From History">
 <!ENTITY cmd.removeFromHistory.accesskey  "e">
 <!ENTITY cmd.clearList.label              "Clear List">
 <!ENTITY cmd.clearList.accesskey          "a">
+<!ENTITY cmd.clearDownloads.label         "Clear Downloads">
+<!ENTITY cmd.clearDownloads.accesskey     "D">
 
 <!-- LOCALIZATION NOTE (downloadsHistory.label, downloadsHistory.accesskey):
      This string is shown at the bottom of the Downloads Panel when all the
      downloads fit in the available space, or when there are no downloads in
      the panel at all.
      -->
 <!ENTITY downloadsHistory.label           "Show All Downloads">
 <!ENTITY downloadsHistory.accesskey       "S">
--- a/docshell/base/nsDownloadHistory.cpp
+++ b/docshell/base/nsDownloadHistory.cpp
@@ -63,8 +63,14 @@ nsDownloadHistory::AddDownload(nsIURI *a
     nsCOMPtr<nsIObserverService> os =
       do_GetService("@mozilla.org/observer-service;1");
     if (os)
       os->NotifyObservers(aSource, NS_LINK_VISITED_EVENT_TOPIC, nullptr);
   }
 
   return NS_OK;
 }
+
+NS_IMETHODIMP
+nsDownloadHistory::RemoveAllDownloads()
+{
+  return NS_ERROR_NOT_IMPLEMENTED;
+}
--- a/docshell/base/nsIDownloadHistory.idl
+++ b/docshell/base/nsIDownloadHistory.idl
@@ -8,17 +8,17 @@
 
 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(a7a3358c-9af2-41e3-adfe-3bf0b7ac2c38)]
+[scriptable, uuid(4dcd6a12-a091-4f38-8360-022929635746)]
 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
@@ -30,14 +30,29 @@ interface nsIDownloadHistory : nsISuppor
    *        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.
+   * @note This addition is not guaranteed to be synchronous, since it delegates
+   *       the actual addition to the underlying history implementation.  If you
+   *       need to observe the completion of the addition, use the underlying
+   *       history implementation's notifications system (e.g. nsINavHistoryObserver
+   *       for toolkit's implementation of this interface).
    */
   void addDownload(in nsIURI aSource, [optional] in nsIURI aReferrer,
                    [optional] in PRTime aStartTime,
                    [optional] in nsIURI aDestination);
+
+  /**
+   * Remove all downloads from history.
+   *
+   * @note This removal is not guaranteed to be synchronous, since it delegates
+   *       the actual removal to the underlying history implementation.  If you
+   *       need to observe the completion of the removal, use the underlying
+   *       history implementation's notifications system (e.g. nsINavHistoryObserver
+   *       for toolkit's implementation of this interface).
+   */
+  void removeAllDownloads();
 };
-
--- a/toolkit/components/downloads/nsDownloadManager.cpp
+++ b/toolkit/components/downloads/nsDownloadManager.cpp
@@ -2373,17 +2373,17 @@ nsDownloadManager::OnPageChanged(nsIURI 
                                  const nsACString &aGUID)
 {
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsDownloadManager::OnDeleteVisits(nsIURI *aURI, PRTime aVisitTime,
                                   const nsACString& aGUID,
-                                  uint16_t aReason)
+                                  uint16_t aReason, uint32_t aTransitionType)
 {
   // Don't bother removing downloads until the page is removed.
   return NS_OK;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 //// nsIObserver
 
--- a/toolkit/components/places/History.cpp
+++ b/toolkit/components/places/History.cpp
@@ -27,20 +27,25 @@
 #include "mozilla/Services.h"
 #include "nsThreadUtils.h"
 #include "nsNetUtil.h"
 #include "nsIXPConnect.h"
 #include "mozilla/unused.h"
 #include "nsContentUtils.h"
 #include "nsIMemoryReporter.h"
 #include "mozilla/ipc/URIUtils.h"
+#include "nsPrintfCString.h"
+#include "nsTHashtable.h"
 
 // Initial size for the cache holding visited status observers.
 #define VISIT_OBSERVERS_INITIAL_CACHE_SIZE 128
 
+// Initial size for the visits removal hash.
+#define VISITS_REMOVAL_INITIAL_HASH_SIZE 128
+
 using namespace mozilla::dom;
 using namespace mozilla::ipc;
 using mozilla::unused;
 
 namespace mozilla {
 namespace places {
 
 ////////////////////////////////////////////////////////////////////////////////
@@ -152,16 +157,65 @@ struct VisitData {
 
   nsCString referrerSpec;
 
   // TODO bug 626836 hook up hidden and typed change tracking too!
   bool titleChanged;
 };
 
 ////////////////////////////////////////////////////////////////////////////////
+//// RemoveVisitsFilter
+
+/**
+ * Used to store visit filters for RemoveVisits.
+ */
+struct RemoveVisitsFilter {
+  RemoveVisitsFilter()
+  : transitionType(UINT32_MAX)
+  {
+  }
+
+  uint32_t transitionType;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+//// PlaceHashKey
+
+class PlaceHashKey : public nsCStringHashKey
+{
+  public:
+    PlaceHashKey(const nsACString& aSpec)
+    : nsCStringHashKey(&aSpec)
+    , visitCount(-1)
+    , bookmarked(-1)
+    {
+    }
+
+    PlaceHashKey(const nsACString* aSpec)
+    : nsCStringHashKey(aSpec)
+    , visitCount(-1)
+    , bookmarked(-1)
+    {
+    }
+
+    PlaceHashKey(const PlaceHashKey& aOther)
+    : nsCStringHashKey(&aOther.GetKey())
+    {
+      MOZ_ASSERT("Do not call me!");
+    }
+
+    // Visit count for this place.
+    int32_t visitCount;
+    // Whether this place is bookmarked.
+    int32_t bookmarked;
+    // Array of VisitData objects.
+    nsTArray<VisitData> visits;
+};
+
+////////////////////////////////////////////////////////////////////////////////
 //// Anonymous Helpers
 
 namespace {
 
 /**
  * Obtains an nsIURI from the "uri" property of a JSObject.
  *
  * @param aCtx
@@ -714,17 +768,17 @@ public:
 
   NS_IMETHOD Run()
   {
     NS_PRECONDITION(!NS_IsMainThread(),
                     "This should not be called on the main thread");
 
     // Prevent the main thread from shutting down while this is running.
     MutexAutoLock lockedScope(mHistory->GetShutdownMutex());
-    if(mHistory->IsShuttingDown()) {
+    if (mHistory->IsShuttingDown()) {
       // If we were already shutting down, we cannot insert the URIs.
       return NS_OK;
     }
 
     mozStorageTransaction transaction(mDBConn, false,
                                       mozIStorageConnection::TRANSACTION_IMMEDIATE);
 
     VisitData* lastPlace = NULL;
@@ -1352,16 +1406,352 @@ private:
   nsRefPtr<History> mHistory;
 };
 NS_IMPL_ISUPPORTS1(
   SetDownloadAnnotations,
   mozIVisitInfoCallback
 )
 
 /**
+ * Enumerator used by NotifyRemoveVisits to transfer the hash entries.
+ */
+static PLDHashOperator TransferHashEntries(PlaceHashKey* aEntry,
+                                           void* aHash)
+{
+  nsTHashtable<PlaceHashKey>* hash =
+    static_cast<nsTHashtable<PlaceHashKey> *>(aHash);
+  PlaceHashKey* copy = hash->PutEntry(aEntry->GetKey());
+  copy->visitCount = aEntry->visitCount;
+  copy->bookmarked = aEntry->bookmarked;
+  aEntry->visits.SwapElements(copy->visits);
+  return PL_DHASH_NEXT;
+}
+
+/**
+ * Enumerator used by NotifyRemoveVisits to notify removals.
+ */
+static PLDHashOperator NotifyVisitRemoval(PlaceHashKey* aEntry,
+                                          void* aHistory)
+{
+  nsNavHistory* history = static_cast<nsNavHistory *>(aHistory);
+  const nsTArray<VisitData>& visits = aEntry->visits;
+  nsCOMPtr<nsIURI> uri;
+  (void)NS_NewURI(getter_AddRefs(uri), visits[0].spec);
+  bool removingPage = visits.Length() == aEntry->visitCount &&
+                      !aEntry->bookmarked;
+  // FindRemovableVisits only sets the transition type on the VisitData objects
+  // it collects if the visits were filtered by transition type.
+  // RemoveVisitsFilter currently only supports filtering by transition type, so
+  // FindRemovableVisits will either find all visits, or all visits of a given
+  // type. Therefore, if transitionType is set on this visit, we pass the
+  // transition type to NotifyOnPageExpired which in turns passes it to
+  // OnDeleteVisits to indicate that all visits of a given type were removed.
+  uint32_t transition = visits[0].transitionType < UINT32_MAX ?
+                          visits[0].transitionType : 0;
+  history->NotifyOnPageExpired(uri, visits[0].visitTime, removingPage,
+                               visits[0].guid,
+                               nsINavHistoryObserver::REASON_DELETED,
+                               transition);
+  return PL_DHASH_NEXT;
+}
+
+/**
+ * Notify removed visits to observers.
+ */
+class NotifyRemoveVisits : public nsRunnable
+{
+public:
+
+  NotifyRemoveVisits(nsTHashtable<PlaceHashKey>& aPlaces)
+  : mHistory(History::GetService())
+  {
+    MOZ_ASSERT(!NS_IsMainThread(),
+               "This should not be called on the main thread");
+    mPlaces.Init(VISITS_REMOVAL_INITIAL_HASH_SIZE);
+    aPlaces.EnumerateEntries(TransferHashEntries, &mPlaces);
+  }
+
+  NS_IMETHOD Run()
+  {
+    MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
+
+    // We are in the main thread, no need to lock.
+    if (mHistory->IsShuttingDown()) {
+      // If we are shutting down, we cannot notify the observers.
+      return NS_OK;
+    }
+
+    nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
+    if (!navHistory) {
+      NS_WARNING("Cannot notify without the history service!");
+      return NS_OK;
+    }
+
+    // Wrap all notifications in a batch, so the view can handle changes in a
+    // more performant way, by initiating a refresh after a limited number of
+    // single changes.
+    (void)navHistory->BeginUpdateBatch();
+    mPlaces.EnumerateEntries(NotifyVisitRemoval, navHistory);
+    (void)navHistory->EndUpdateBatch();
+
+    return NS_OK;
+  }
+
+private:
+  nsTHashtable<PlaceHashKey> mPlaces;
+
+  /**
+   * Strong reference to the History object because we do not want it to
+   * disappear out from under us.
+   */
+  nsRefPtr<History> mHistory;
+};
+
+/**
+ * Enumerator used by RemoveVisits to populate list of removed place ids.
+ */
+static PLDHashOperator ListToBeRemovedPlaceIds(PlaceHashKey* aEntry,
+                                               void* aIdsList)
+{
+  const nsTArray<VisitData>& visits = aEntry->visits;
+  // Only orphan ids should be listed.
+  if (visits.Length() == aEntry->visitCount && !aEntry->bookmarked) {
+    nsCString* list = static_cast<nsCString*>(aIdsList);
+    if (!list->IsEmpty())
+      list->AppendLiteral(",");
+    list->AppendInt(visits[0].placeId);
+  }
+  return PL_DHASH_NEXT;
+}
+
+/**
+ * Remove visits from history.
+ */
+class RemoveVisits : public nsRunnable
+{
+public:
+  /**
+   * Asynchronously removes visits from history.
+   *
+   * @param aConnection
+   *        The database connection to use for these operations.
+   * @param aFilter
+   *        Filter to remove visits.
+   */
+  static nsresult Start(mozIStorageConnection* aConnection,
+                        RemoveVisitsFilter& aFilter)
+  {
+    MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
+
+    nsRefPtr<RemoveVisits> event = new RemoveVisits(aConnection, aFilter);
+
+    // Get the target thread, and then start the work!
+    nsCOMPtr<nsIEventTarget> target = do_GetInterface(aConnection);
+    NS_ENSURE_TRUE(target, NS_ERROR_UNEXPECTED);
+    nsresult rv = target->Dispatch(event, NS_DISPATCH_NORMAL);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    return NS_OK;
+  }
+
+  NS_IMETHOD Run()
+  {
+    MOZ_ASSERT(!NS_IsMainThread(),
+               "This should not be called on the main thread");
+
+    // Prevent the main thread from shutting down while this is running.
+    MutexAutoLock lockedScope(mHistory->GetShutdownMutex());
+    if (mHistory->IsShuttingDown()) {
+      // If we were already shutting down, we cannot remove the visits.
+      return NS_OK;
+    }
+
+    // Find all the visits relative to the current filters and whether their
+    // pages will be removed or not.
+    nsTHashtable<PlaceHashKey> places;
+    places.Init(VISITS_REMOVAL_INITIAL_HASH_SIZE);
+    nsresult rv = FindRemovableVisits(places);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    if (places.Count() == 0)
+      return NS_OK;
+
+    // TODO (bug 826409): should notify onBeforeDeleteURI, but that's
+    // complicated off main-thread.  OnBefore notifications should be removed.
+
+    mozStorageTransaction transaction(mDBConn, false,
+                                      mozIStorageConnection::TRANSACTION_IMMEDIATE);
+
+    rv = RemoveVisitsFromDatabase();
+    NS_ENSURE_SUCCESS(rv, rv);
+    rv = RemovePagesFromDatabase(places);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    rv = transaction.Commit();
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    nsCOMPtr<nsIRunnable> event = new NotifyRemoveVisits(places);
+    rv = NS_DispatchToMainThread(event);
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    return NS_OK;
+  }
+
+private:
+  RemoveVisits(mozIStorageConnection* aConnection,
+               RemoveVisitsFilter& aFilter)
+  : mDBConn(aConnection)
+  , mHasTransitionType(false)
+  , mHistory(History::GetService())
+  {
+    MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
+
+    // Build query conditions.
+    nsTArray<nsCString> conditions;
+    // TODO: add support for binding params when adding further stuff here.
+    if (aFilter.transitionType < UINT32_MAX) {
+      conditions.AppendElement(nsPrintfCString("visit_type = %d", aFilter.transitionType));
+      mHasTransitionType = true;
+    }
+    if (conditions.Length() > 0) {
+      mWhereClause.AppendLiteral (" WHERE ");
+      for (uint32_t i = 0; i < conditions.Length(); ++i) {
+        if (i > 0)
+          mWhereClause.AppendLiteral(" AND ");
+        mWhereClause.Append(conditions[i]);
+      }
+    }
+  }
+
+  nsresult
+  FindRemovableVisits(nsTHashtable<PlaceHashKey>& aPlaces)
+  {
+    MOZ_ASSERT(!NS_IsMainThread(),
+               "This should not be called on the main thread");
+
+    nsCString query("SELECT h.id, url, guid, visit_date, visit_type, "
+                    "(SELECT count(*) FROM moz_historyvisits WHERE place_id = h.id) as full_visit_count, "
+                    "EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) as bookmarked "
+                    "FROM moz_historyvisits "
+                    "JOIN moz_places h ON place_id = h.id");
+    query.Append(mWhereClause);
+
+    nsCOMPtr<mozIStorageStatement> stmt = mHistory->GetStatement(query);
+    NS_ENSURE_STATE(stmt);
+    mozStorageStatementScoper scoper(stmt);
+
+    bool hasResult;
+    nsresult rv;
+    while (NS_SUCCEEDED((rv = stmt->ExecuteStep(&hasResult))) && hasResult) {
+      VisitData visit;
+      rv = stmt->GetInt64(0, &visit.placeId);
+      NS_ENSURE_SUCCESS(rv, rv);
+      rv = stmt->GetUTF8String(1, visit.spec);
+      NS_ENSURE_SUCCESS(rv, rv);
+      rv = stmt->GetUTF8String(2, visit.guid);
+      NS_ENSURE_SUCCESS(rv, rv);
+      rv = stmt->GetInt64(3, &visit.visitTime);
+      NS_ENSURE_SUCCESS(rv, rv);
+      if (mHasTransitionType) {
+        int32_t transition;
+        rv = stmt->GetInt32(4, &transition);
+        NS_ENSURE_SUCCESS(rv, rv);
+        visit.transitionType = static_cast<uint32_t>(transition);
+      }
+      int32_t visitCount, bookmarked;
+      rv = stmt->GetInt32(5, &visitCount);
+      NS_ENSURE_SUCCESS(rv, rv);
+      rv = stmt->GetInt32(6, &bookmarked);
+      NS_ENSURE_SUCCESS(rv, rv);
+
+      PlaceHashKey* entry = aPlaces.GetEntry(visit.spec);
+      if (!entry) {
+        entry = aPlaces.PutEntry(visit.spec);
+      }
+      entry->visitCount = visitCount;
+      entry->bookmarked = bookmarked;
+      entry->visits.AppendElement(visit);
+    }
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    return NS_OK;
+  }
+
+  nsresult
+  RemoveVisitsFromDatabase()
+  {
+    MOZ_ASSERT(!NS_IsMainThread(),
+               "This should not be called on the main thread");
+
+    nsCString query("DELETE FROM moz_historyvisits");
+    query.Append(mWhereClause);
+
+    nsCOMPtr<mozIStorageStatement> stmt = mHistory->GetStatement(query);
+    NS_ENSURE_STATE(stmt);
+    mozStorageStatementScoper scoper(stmt);
+    nsresult rv = stmt->Execute();
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    return NS_OK;
+  }
+
+  nsresult
+  RemovePagesFromDatabase(nsTHashtable<PlaceHashKey>& aPlaces)
+  {
+    MOZ_ASSERT(!NS_IsMainThread(),
+               "This should not be called on the main thread");
+
+    nsCString placeIdsToRemove;
+    aPlaces.EnumerateEntries(ListToBeRemovedPlaceIds, &placeIdsToRemove);
+
+#ifdef DEBUG
+    {
+      // Ensure that we are not removing any problematic entry.
+      nsCString query("SELECT id FROM moz_places h WHERE id IN (");
+      query.Append(placeIdsToRemove);
+      query.AppendLiteral(") AND ("
+          "EXISTS(SELECT 1 FROM moz_bookmarks WHERE fk = h.id) OR "
+          "EXISTS(SELECT 1 FROM moz_historyvisits WHERE place_id = h.id) OR "
+          "SUBSTR(h.url, 1, 6) = 'place:' "
+        ")");
+      nsCOMPtr<mozIStorageStatement> stmt = mHistory->GetStatement(query);
+      NS_ENSURE_STATE(stmt);
+      mozStorageStatementScoper scoper(stmt);
+      bool hasResult;
+      MOZ_ASSERT(NS_SUCCEEDED(stmt->ExecuteStep(&hasResult)) && !hasResult,
+                 "Trying to remove a non-oprhan place from the database");
+    }
+#endif
+
+    nsCString query("DELETE FROM moz_places "
+                    "WHERE id IN (");
+    query.Append(placeIdsToRemove);
+    query.AppendLiteral(")");
+
+    nsCOMPtr<mozIStorageStatement> stmt = mHistory->GetStatement(query);
+    NS_ENSURE_STATE(stmt);
+    mozStorageStatementScoper scoper(stmt);
+    nsresult rv = stmt->Execute();
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    return NS_OK;
+  }
+
+  mozIStorageConnection* mDBConn;
+  bool mHasTransitionType;
+  nsCString mWhereClause;
+
+  /**
+   * Strong reference to the History object because we do not want it to
+   * disappear out from under us.
+   */
+  nsRefPtr<History> mHistory;
+};
+
+/**
  * Stores an embed visit, and notifies observers.
  *
  * @param aPlace
  *        The VisitData of the visit to store as an embed visit.
  * @param [optional] aCallback
  *        The mozIVisitInfoCallback to notify, if provided.
  */
 void
@@ -2114,16 +2504,45 @@ History::AddDownload(nsIURI* aSource, ns
     mozilla::services::GetObserverService();
   if (obsService) {
     obsService->NotifyObservers(aSource, NS_LINK_VISITED_EVENT_TOPIC, nullptr);
   }
 
   return NS_OK;
 }
 
+NS_IMETHODIMP
+History::RemoveAllDownloads()
+{
+  MOZ_ASSERT(NS_IsMainThread());
+
+  if (mShuttingDown) {
+    return NS_OK;
+  }
+
+  if (XRE_GetProcessType() == GeckoProcessType_Content) {
+    NS_ERROR("Cannot remove downloads to history from content process!");
+    return NS_ERROR_NOT_AVAILABLE;
+  }
+
+  // Ensure navHistory is initialized.
+  nsNavHistory* navHistory = nsNavHistory::GetHistoryService();
+  NS_ENSURE_TRUE(navHistory, NS_ERROR_OUT_OF_MEMORY);
+  mozIStorageConnection* dbConn = GetDBConn();
+  NS_ENSURE_STATE(dbConn);
+
+  RemoveVisitsFilter filter;
+  filter.transitionType = nsINavHistoryService::TRANSITION_DOWNLOAD;
+
+  nsresult rv = RemoveVisits::Start(dbConn, filter);
+  NS_ENSURE_SUCCESS(rv, rv);
+
+  return NS_OK;
+}
+
 ////////////////////////////////////////////////////////////////////////////////
 //// mozIAsyncHistory
 
 NS_IMETHODIMP
 History::UpdatePlaces(const jsval& aPlaceInfos,
                       mozIVisitInfoCallback* aCallback,
                       JSContext* aCtx)
 {
--- a/toolkit/components/places/History.h
+++ b/toolkit/components/places/History.h
@@ -98,16 +98,24 @@ public:
   already_AddRefed<mozIStorageStatement>
   GetStatement(const char (&aQuery)[N])
   {
     mozIStorageConnection* dbConn = GetDBConn();
     NS_ENSURE_TRUE(dbConn, nullptr);
     return mDB->GetStatement(aQuery);
   }
 
+  already_AddRefed<mozIStorageStatement>
+  GetStatement(const nsACString& aQuery)
+  {
+    mozIStorageConnection* dbConn = GetDBConn();
+    NS_ENSURE_TRUE(dbConn, nullptr);
+    return mDB->GetStatement(aQuery);
+  }
+
   bool IsShuttingDown() const {
     return mShuttingDown;
   }
   Mutex& GetShutdownMutex() {
     return mShutdownMutex;
   }
 
   /**
--- a/toolkit/components/places/nsINavHistoryService.idl
+++ b/toolkit/components/places/nsINavHistoryService.idl
@@ -645,17 +645,17 @@ interface nsINavHistoryResult : nsISuppo
 
 /**
  * Similar to nsIRDFObserver for history. Note that we don't pass the data
  * source since that is always the global history.
  *
  * DANGER! If you are in the middle of a batch transaction, there may be a
  * database transaction active. You can still access the DB, but be careful.
  */
-[scriptable, uuid(eb264079-8766-4e66-b9bf-2c8b586c74d3)]
+[scriptable, uuid(e4d40863-ee3c-4094-9fc0-18c3245d97ef)]
 interface nsINavHistoryObserver : nsISupports
 {
   /**
    * Notifies you that a bunch of things are about to change, don't do any
    * heavy-duty processing until onEndUpdateBatch is called.
    */
   void onBeginUpdateBatch();
 
@@ -800,21 +800,25 @@ interface nsINavHistoryObserver : nsISup
    *        The unique ID associated with the page.
    *
    * @note: when all visits for a page are expired and also the full page entry
    *        is expired, you will only get an onDeleteURI notification.  If a
    *        page entry is removed, then you can be sure that we don't have
    *        anymore visits for it.
    * @param aReason
    *        Indicates the reason for the removal.  see REASON_* constants.
+   * @param aTransitionType
+   *        If it's a valid TRANSITION_* value, all visits of the specified type
+   *        have been removed.
    */
   void onDeleteVisits(in nsIURI aURI,
                       in PRTime aVisitTime,
                       in ACString aGUID,
-                      in unsigned short aReason);
+                      in unsigned short aReason,
+                      in unsigned long aTransitionType);
 };
 
 
 /**
  * This object encapsulates all the query parameters you're likely to need
  * when building up history UI. All parameters are ANDed together.
  *
  * This is not intended to be a super-general query mechanism. This was designed
--- a/toolkit/components/places/nsNavBookmarks.cpp
+++ b/toolkit/components/places/nsNavBookmarks.cpp
@@ -2885,17 +2885,17 @@ nsNavBookmarks::OnPageChanged(nsIURI* aU
   }
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavBookmarks::OnDeleteVisits(nsIURI* aURI, PRTime aVisitTime,
                                const nsACString& aGUID,
-                               uint16_t aReason)
+                               uint16_t aReason, uint32_t aTransitionType)
 {
   // Notify "cleartime" only if all visits to the page have been removed.
   if (!aVisitTime) {
     // If the page is bookmarked, notify observers for each associated bookmark.
     ItemChangeData changeData;
     nsresult rv = aURI->GetSpec(changeData.bookmark.url);
     NS_ENSURE_SUCCESS(rv, rv);
     changeData.property = NS_LITERAL_CSTRING("cleartime");
--- a/toolkit/components/places/nsNavHistory.cpp
+++ b/toolkit/components/places/nsNavHistory.cpp
@@ -2729,17 +2729,17 @@ nsNavHistory::CleanupPlacesOnVisitsDelet
                        nsINavHistoryObserver,
                        OnBeforeDeleteURI(uri, guid, nsINavHistoryObserver::REASON_DELETED));
     }
     else {
       // Notify that we will delete all visits for this page, but not the page
       // itself, since it's bookmarked or a place: query.
       NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
                        nsINavHistoryObserver,
-                       OnDeleteVisits(uri, 0, guid, nsINavHistoryObserver::REASON_DELETED));
+                       OnDeleteVisits(uri, 0, guid, nsINavHistoryObserver::REASON_DELETED, 0));
     }
   }
 
   // if the entry is not bookmarked and is not a place: uri
   // then we can remove it from moz_places.
   // Note that we do NOT delete favicons. Any unreferenced favicons will be
   // deleted next time the browser is shut down.
   nsresult rv = mDB->MainConn()->ExecuteSimpleSQL(
@@ -3612,32 +3612,33 @@ nsNavHistory::AsyncExecuteLegacyQueries(
 }
 
 
 // nsPIPlacesHistoryListenersNotifier ******************************************
 
 NS_IMETHODIMP
 nsNavHistory::NotifyOnPageExpired(nsIURI *aURI, PRTime aVisitTime,
                                   bool aWholeEntry, const nsACString& aGUID,
-                                  uint16_t aReason)
+                                  uint16_t aReason, uint32_t aTransitionType)
 {
   // Invalidate the cached value for whether there's history or not.
   mHasHistoryEntries = -1;
 
   MOZ_ASSERT(!aGUID.IsEmpty());
   if (aWholeEntry) {
     // Notify our observers that the page has been removed.
     NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
                      nsINavHistoryObserver, OnDeleteURI(aURI, aGUID, aReason));
   }
   else {
     // Notify our observers that some visits for the page have been removed.
     NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
                      nsINavHistoryObserver,
-                     OnDeleteVisits(aURI, aVisitTime, aGUID, aReason));
+                     OnDeleteVisits(aURI, aVisitTime, aGUID, aReason,
+                                    aTransitionType));
   }
 
   return NS_OK;
 }
 
 ////////////////////////////////////////////////////////////////////////////////
 //// nsIObserver
 
--- a/toolkit/components/places/nsNavHistoryResult.cpp
+++ b/toolkit/components/places/nsNavHistoryResult.cpp
@@ -2852,27 +2852,37 @@ nsNavHistoryQueryResultNode::OnPageChang
   return NS_OK;
 }
 
 
 NS_IMETHODIMP
 nsNavHistoryQueryResultNode::OnDeleteVisits(nsIURI* aURI,
                                             PRTime aVisitTime,
                                             const nsACString& aGUID,
-                                            uint16_t aReason)
+                                            uint16_t aReason,
+                                            uint32_t aTransitionType)
 {
   NS_PRECONDITION(mOptions->QueryType() == nsINavHistoryQueryOptions::QUERY_TYPE_HISTORY,
                   "Bookmarks queries should not get a OnDeleteVisits notification");
   if (aVisitTime == 0) {
     // All visits for this uri have been removed, but the uri won't be removed
     // from the databse, most likely because it's a bookmark.  For a history
     // query this is equivalent to a onDeleteURI notification.
     nsresult rv = OnDeleteURI(aURI, aGUID, aReason);
     NS_ENSURE_SUCCESS(rv, rv);
   }
+  if (aTransitionType > 0) {
+    // All visits for aTransitionType have been removed, if the query is
+    // filtering on such transition type, this is equivalent to an onDeleteURI
+    // notification.
+    if ((mQueries[0]->Transitions()).Contains(aTransitionType)) {
+      nsresult rv = OnDeleteURI(aURI, aGUID, aReason);
+      NS_ENSURE_SUCCESS(rv, rv);
+    }
+  }
 
   return NS_OK;
 }
 
 nsresult
 nsNavHistoryQueryResultNode::NotifyIfTagsChanged(nsIURI* aURI)
 {
   nsNavHistoryResult* result = GetResult();
@@ -4832,13 +4842,15 @@ nsNavHistoryResult::OnPageChanged(nsIURI
 
 /**
  * Don't do anything when visits expire.
  */
 NS_IMETHODIMP
 nsNavHistoryResult::OnDeleteVisits(nsIURI* aURI,
                                    PRTime aVisitTime,
                                    const nsACString& aGUID,
-                                   uint16_t aReason)
+                                   uint16_t aReason,
+                                   uint32_t aTransitionType)
 {
-  ENUMERATE_HISTORY_OBSERVERS(OnDeleteVisits(aURI, aVisitTime, aGUID, aReason));
+  ENUMERATE_HISTORY_OBSERVERS(OnDeleteVisits(aURI, aVisitTime, aGUID, aReason,
+                                             aTransitionType));
   return NS_OK;
 }
--- a/toolkit/components/places/nsNavHistoryResult.h
+++ b/toolkit/components/places/nsNavHistoryResult.h
@@ -69,17 +69,18 @@ private:
                                uint16_t aReason);                       \
   NS_IMETHOD OnDeleteURI(nsIURI *aURI, const nsACString& aGUID,         \
                          uint16_t aReason);                             \
   NS_IMETHOD OnClearHistory();                                          \
   NS_IMETHOD OnPageChanged(nsIURI *aURI, uint32_t aChangedAttribute,    \
                            const nsAString &aNewValue,                  \
                            const nsACString &aGUID);                    \
   NS_IMETHOD OnDeleteVisits(nsIURI* aURI, PRTime aVisitTime,            \
-                            const nsACString& aGUID, uint16_t aReason);
+                            const nsACString& aGUID, uint16_t aReason,  \
+                            uint32_t aTransitionType);
 
 // The internal version has an output aAdded parameter, it is incremented by
 // query nodes when the visited uri belongs to them. If no such query exists,
 // the history result creates a new query node dynamically.
 #define NS_DECL_BOOKMARK_HISTORY_OBSERVER_INTERNAL                      \
   NS_DECL_BOOKMARK_HISTORY_OBSERVER_BASE                                \
   NS_IMETHOD OnVisit(nsIURI* aURI, int64_t aVisitId, PRTime aTime,      \
                      int64_t aSessionId, int64_t aReferringId,          \
--- a/toolkit/components/places/nsPIPlacesHistoryListenersNotifier.idl
+++ b/toolkit/components/places/nsPIPlacesHistoryListenersNotifier.idl
@@ -10,17 +10,17 @@ interface nsIURI;
 
 /**
  * This is a private interface used by Places components to notify history
  * listeners about important notifications.  These should not be used by any
  * code that is not part of core.
  *
  * @note See also: nsINavHistoryObserver
  */
-[scriptable, uuid(3b0953cd-f483-4908-8d91-282b6bda0453)]
+[scriptable, uuid(808cf36c-4c9a-4bdb-91a4-d60a6fc25add)]
 interface nsPIPlacesHistoryListenersNotifier : nsISupports
 {
   /**
    * Calls onDeleteVisits and onDeleteURI notifications on registered listeners
    * with the history service.
    *
    * @param aURI
    *        The nsIURI object representing the URI of the page being expired.
@@ -28,15 +28,19 @@ interface nsPIPlacesHistoryListenersNoti
    *        The time, in microseconds, that the page being expired was visited.
    * @param aWholeEntry
    *        Indicates if this is the last visit for this URI.
    * @param aGUID
    *        The unique ID associated with the page.
    * @param aReason
    *        Indicates the reason for the removal.
    *        See nsINavHistoryObserver::REASON_* constants.
+   * @param aTransitionType
+   *        If it's a valid TRANSITION_* value, all visits of the specified type
+   *        have been removed.
    */
   void notifyOnPageExpired(in nsIURI aURI,
                            in PRTime aVisitTime,
                            in boolean aWholeEntry,
                            in ACString aGUID,
-                           in unsigned short aReason);
+                           in unsigned short aReason,
+                           in unsigned long aTransitionType);
 };
--- a/toolkit/components/places/nsPlacesExpiration.js
+++ b/toolkit/components/places/nsPlacesExpiration.js
@@ -626,17 +626,17 @@ nsPlacesExpiration.prototype = {
         this._expectedResultsCount--;
 
       let uri = Services.io.newURI(row.getResultByName("url"), null, null);
       let guid = row.getResultByName("guid");
       let visitDate = row.getResultByName("visit_date");
       let wholeEntry = row.getResultByName("whole_entry");
       // Dispatch expiration notifications to history.
       this._hsn.notifyOnPageExpired(uri, visitDate, wholeEntry, guid,
-                                    Ci.nsINavHistoryObserver.REASON_EXPIRED);
+                                    Ci.nsINavHistoryObserver.REASON_EXPIRED, 0);
     }
   },
 
   handleError: function PEX_handleError(aError)
   {
     Cu.reportError("Async statement execution returned with '" +
                    aError.result + "', '" + aError.message + "'");
   },
--- a/toolkit/components/places/tests/head_common.js
+++ b/toolkit/components/places/tests/head_common.js
@@ -427,17 +427,17 @@ function promiseTopicObserved(aTopic)
  * Clears history asynchronously.
  *
  * @return {Promise}
  * @resolves When history has been cleared.
  * @rejects Never.
  */
 function promiseClearHistory() {
   let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED);
-  PlacesUtils.bhistory.removeAllPages();
+  do_execute_soon(function() PlacesUtils.bhistory.removeAllPages());
   return promise;
 }
 
 
 /**
  * Simulates a Places shutdown.
  */
 function shutdownPlaces(aKeepAliveConnection)
--- a/toolkit/components/places/tests/unit/test_download_history.js
+++ b/toolkit/components/places/tests/unit/test_download_history.js
@@ -1,98 +1,147 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 /**
- * This file tests the nsIDownloadHistory interface.
+ * This file tests the nsIDownloadHistory Places implementation.
  */
 
-////////////////////////////////////////////////////////////////////////////////
-/// Globals
-
 XPCOMUtils.defineLazyServiceGetter(this, "gDownloadHistory",
                                    "@mozilla.org/browser/download-history;1",
                                    "nsIDownloadHistory");
 
-XPCOMUtils.defineLazyServiceGetter(this, "gHistory",
-                                   "@mozilla.org/browser/history;1",
-                                   "mozIAsyncHistory");
-
 const DOWNLOAD_URI = NetUtil.newURI("http://www.example.com/");
 const REFERRER_URI = NetUtil.newURI("http://www.example.org/");
 const PRIVATE_URI = NetUtil.newURI("http://www.example.net/");
 
 /**
  * Waits for the first visit notification to be received.
  *
  * @param aCallback
- *        This function is called with the same arguments of onVisit.
+ *        Callback function to be called with the same arguments of onVisit.
  */
 function waitForOnVisit(aCallback) {
   let historyObserver = {
     __proto__: NavHistoryObserver.prototype,
     onVisit: function HO_onVisit() {
       PlacesUtils.history.removeObserver(this);
       aCallback.apply(null, arguments);
     }
   };
   PlacesUtils.history.addObserver(historyObserver, false);
 }
 
 /**
- * Checks to see that a URI is in the database.
+ * Waits for the first onDeleteURI notification to be received.
  *
- * @param aURI
- *        The URI to check.
- * @param aExpected
- *        Boolean result expected from the db lookup.
+ * @param aCallback
+ *        Callback function to be called with the same arguments of onDeleteURI.
  */
-function uri_in_db(aURI, aExpected)
-{
-  let options = PlacesUtils.history.getNewQueryOptions();
-  options.maxResults = 1;
-  options.includeHidden = true;
-
-  let query = PlacesUtils.history.getNewQuery();
-  query.uri = aURI;
-
-  let root = PlacesUtils.history.executeQuery(query, options).root;
-  root.containerOpen = true;
-
-  do_check_eq(root.childCount, aExpected ? 1 : 0);
-
-  // Close the container explicitly to free resources up earlier.
-  root.containerOpen = false;
+function waitForOnDeleteURI(aCallback) {
+  let historyObserver = {
+    __proto__: NavHistoryObserver.prototype,
+    onDeleteURI: function HO_onDeleteURI() {
+      PlacesUtils.history.removeObserver(this);
+      aCallback.apply(null, arguments);
+    }
+  };
+  PlacesUtils.history.addObserver(historyObserver, false);
 }
 
-////////////////////////////////////////////////////////////////////////////////
-/// Tests
+/**
+ * Waits for the first onDeleteVisits notification to be received.
+ *
+ * @param aCallback
+ *        Callback function to be called with the same arguments of onDeleteVisits.
+ */
+function waitForOnDeleteVisits(aCallback) {
+  let historyObserver = {
+    __proto__: NavHistoryObserver.prototype,
+    onDeleteVisits: function HO_onDeleteVisits() {
+      PlacesUtils.history.removeObserver(this);
+      aCallback.apply(null, arguments);
+    }
+  };
+  PlacesUtils.history.addObserver(historyObserver, false);
+}
 
 function run_test()
 {
   run_next_test();
 }
 
 add_test(function test_dh_is_from_places()
 {
   // Test that this nsIDownloadHistory is the one places implements.
   do_check_true(gDownloadHistory instanceof Ci.mozIAsyncHistory);
 
-  promiseClearHistory().then(run_next_test);
+  run_next_test();
 });
 
-add_test(function test_dh_addDownload()
+add_test(function test_dh_addRemoveDownload()
 {
   waitForOnVisit(function DHAD_onVisit(aURI) {
     do_check_true(aURI.equals(DOWNLOAD_URI));
 
     // Verify that the URI is already available in results at this time.
-    uri_in_db(DOWNLOAD_URI, true);
+    do_check_true(!!page_in_database(DOWNLOAD_URI));
+
+    waitForOnDeleteURI(function DHRAD_onDeleteURI(aURI) {
+      do_check_true(aURI.equals(DOWNLOAD_URI));
+
+      // Verify that the URI is already available in results at this time.
+      do_check_false(!!page_in_database(DOWNLOAD_URI));
+
+      run_next_test();
+    });
+    gDownloadHistory.removeAllDownloads();
+  });
+
+  gDownloadHistory.addDownload(DOWNLOAD_URI, null, Date.now() * 1000);
+});
+
+add_test(function test_dh_addMultiRemoveDownload()
+{
+  promiseAddVisits({ uri: DOWNLOAD_URI,
+                     transition: TRANSITION_TYPED }).then(function () {
+    waitForOnVisit(function DHAD_onVisit(aURI) {
+      do_check_true(aURI.equals(DOWNLOAD_URI));
+      do_check_true(!!page_in_database(DOWNLOAD_URI));
 
-    promiseClearHistory().then(run_next_test);
+      waitForOnDeleteVisits(function DHRAD_onDeleteVisits(aURI) {
+        do_check_true(aURI.equals(DOWNLOAD_URI));
+        do_check_true(!!page_in_database(DOWNLOAD_URI));
+
+        promiseClearHistory().then(run_next_test);
+      });
+      gDownloadHistory.removeAllDownloads();
+    });
+
+    gDownloadHistory.addDownload(DOWNLOAD_URI, null, Date.now() * 1000);
+  });
+});
+
+add_test(function test_dh_addBookmarkRemoveDownload()
+{
+  PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+                                       DOWNLOAD_URI,
+                                       PlacesUtils.bookmarks.DEFAULT_INDEX,
+                                       "A bookmark");
+  waitForOnVisit(function DHAD_onVisit(aURI) {
+    do_check_true(aURI.equals(DOWNLOAD_URI));
+    do_check_true(!!page_in_database(DOWNLOAD_URI));
+
+    waitForOnDeleteVisits(function DHRAD_onDeleteVisits(aURI) {
+      do_check_true(aURI.equals(DOWNLOAD_URI));
+      do_check_true(!!page_in_database(DOWNLOAD_URI));
+
+      promiseClearHistory().then(run_next_test);
+    });
+    gDownloadHistory.removeAllDownloads();
   });
 
   gDownloadHistory.addDownload(DOWNLOAD_URI, null, Date.now() * 1000);
 });
 
 add_test(function test_dh_addDownload_referrer()
 {
   waitForOnVisit(function DHAD_prepareReferrer(aURI, aVisitID) {
@@ -100,27 +149,27 @@ add_test(function test_dh_addDownload_re
     let referrerVisitId = aVisitID;
 
     waitForOnVisit(function DHAD_onVisit(aURI, aVisitID, aTime, aSessionID,
                                               aReferringID) {
       do_check_true(aURI.equals(DOWNLOAD_URI));
       do_check_eq(aReferringID, referrerVisitId);
 
       // Verify that the URI is already available in results at this time.
-      uri_in_db(DOWNLOAD_URI, true);
+      do_check_true(!!page_in_database(DOWNLOAD_URI));
 
       promiseClearHistory().then(run_next_test);
     });
 
     gDownloadHistory.addDownload(DOWNLOAD_URI, REFERRER_URI, Date.now() * 1000);
   });
 
   // Note that we don't pass the optional callback argument here because we must
   // ensure that we receive the onVisit notification before we call addDownload.
-  gHistory.updatePlaces({
+  PlacesUtils.asyncHistory.updatePlaces({
     uri: REFERRER_URI,
     visits: [{
       transitionType: Ci.nsINavHistoryService.TRANSITION_TYPED,
       visitDate: Date.now() * 1000
     }]
   });
 });
 
@@ -128,18 +177,18 @@ add_test(function test_dh_addDownload_di
 {
   waitForOnVisit(function DHAD_onVisit(aURI) {
     // We should only receive the notification for the non-private URI.  This
     // test is based on the assumption that visit notifications are received in
     // the same order of the addDownload calls, which is currently true because
     // database access is serialized on the same worker thread.
     do_check_true(aURI.equals(DOWNLOAD_URI));
 
-    uri_in_db(DOWNLOAD_URI, true);
-    uri_in_db(PRIVATE_URI, false);
+    do_check_true(!!page_in_database(DOWNLOAD_URI));
+    do_check_false(!!page_in_database(PRIVATE_URI));
 
     promiseClearHistory().then(run_next_test);
   });
 
   Services.prefs.setBoolPref("places.history.enabled", false);
   gDownloadHistory.addDownload(PRIVATE_URI, REFERRER_URI, Date.now() * 1000);
 
   // The addDownload functions calls CanAddURI synchronously, thus we can set