зеркало из https://github.com/mozilla/gecko-dev.git
828 строки
27 KiB
JavaScript
828 строки
27 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
/**
|
|
* Provides access to downloads from previous sessions on platforms that store
|
|
* them in a different location than session downloads.
|
|
*
|
|
* This module works with objects that are compatible with Download, while using
|
|
* the Places interfaces internally. Some of the Places objects may also be
|
|
* exposed to allow the consumers to integrate with history view commands.
|
|
*/
|
|
|
|
"use strict";
|
|
|
|
var EXPORTED_SYMBOLS = ["DownloadHistory"];
|
|
|
|
const { DownloadList } = ChromeUtils.import(
|
|
"resource://gre/modules/DownloadList.jsm"
|
|
);
|
|
const { XPCOMUtils } = ChromeUtils.import(
|
|
"resource://gre/modules/XPCOMUtils.jsm"
|
|
);
|
|
|
|
XPCOMUtils.defineLazyModuleGetters(this, {
|
|
Downloads: "resource://gre/modules/Downloads.jsm",
|
|
FileUtils: "resource://gre/modules/FileUtils.jsm",
|
|
OS: "resource://gre/modules/osfile.jsm",
|
|
PlacesUtils: "resource://gre/modules/PlacesUtils.jsm",
|
|
Services: "resource://gre/modules/Services.jsm",
|
|
});
|
|
|
|
// Places query used to retrieve all history downloads for the related list.
|
|
const HISTORY_PLACES_QUERY =
|
|
"place:transition=" +
|
|
Ci.nsINavHistoryService.TRANSITION_DOWNLOAD +
|
|
"&sort=" +
|
|
Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING;
|
|
|
|
const DESTINATIONFILEURI_ANNO = "downloads/destinationFileURI";
|
|
const METADATA_ANNO = "downloads/metaData";
|
|
|
|
const METADATA_STATE_FINISHED = 1;
|
|
const METADATA_STATE_FAILED = 2;
|
|
const METADATA_STATE_CANCELED = 3;
|
|
const METADATA_STATE_PAUSED = 4;
|
|
const METADATA_STATE_BLOCKED_PARENTAL = 6;
|
|
const METADATA_STATE_DIRTY = 8;
|
|
|
|
/**
|
|
* Provides methods to retrieve downloads from previous sessions and store
|
|
* downloads for future sessions.
|
|
*/
|
|
var DownloadHistory = {
|
|
/**
|
|
* Retrieves the main DownloadHistoryList object which provides a unified view
|
|
* on downloads from both previous browsing sessions and this session.
|
|
*
|
|
* @param type
|
|
* Determines which type of downloads from this session should be
|
|
* included in the list. This is Downloads.PUBLIC by default, but can
|
|
* also be Downloads.PRIVATE or Downloads.ALL.
|
|
* @param maxHistoryResults
|
|
* Optional number that limits the amount of results the history query
|
|
* may return.
|
|
*
|
|
* @return {Promise}
|
|
* @resolves The requested DownloadHistoryList object.
|
|
* @rejects JavaScript exception.
|
|
*/
|
|
async getList({ type = Downloads.PUBLIC, maxHistoryResults } = {}) {
|
|
await DownloadCache.ensureInitialized();
|
|
|
|
let key = `${type}|${maxHistoryResults ? maxHistoryResults : -1}`;
|
|
if (!this._listPromises[key]) {
|
|
this._listPromises[key] = Downloads.getList(type).then(list => {
|
|
// When the amount of history downloads is capped, we request the list in
|
|
// descending order, to make sure that the list can apply the limit.
|
|
let query =
|
|
HISTORY_PLACES_QUERY +
|
|
(maxHistoryResults ? "&maxResults=" + maxHistoryResults : "");
|
|
return new DownloadHistoryList(list, query);
|
|
});
|
|
}
|
|
|
|
return this._listPromises[key];
|
|
},
|
|
|
|
/**
|
|
* This object is populated with one key for each type of download list that
|
|
* can be returned by the getList method. The values are promises that resolve
|
|
* to DownloadHistoryList objects.
|
|
*/
|
|
_listPromises: {},
|
|
|
|
async addDownloadToHistory(download) {
|
|
if (
|
|
download.source.isPrivate ||
|
|
!PlacesUtils.history.canAddURI(PlacesUtils.toURI(download.source.url))
|
|
) {
|
|
return;
|
|
}
|
|
|
|
await DownloadCache.addDownload(download);
|
|
|
|
await this._updateHistoryListData(download.source.url);
|
|
},
|
|
|
|
/**
|
|
* Stores new detailed metadata for the given download in history. This is
|
|
* normally called after a download finishes, fails, or is canceled.
|
|
*
|
|
* Failed or canceled downloads with partial data are not stored as paused,
|
|
* because the information from the session download is required for resuming.
|
|
*
|
|
* @param download
|
|
* Download object whose metadata should be updated. If the object
|
|
* represents a private download, the call has no effect.
|
|
*/
|
|
async updateMetaData(download) {
|
|
if (download.source.isPrivate || !download.stopped) {
|
|
return;
|
|
}
|
|
|
|
let state = METADATA_STATE_CANCELED;
|
|
if (download.succeeded) {
|
|
state = METADATA_STATE_FINISHED;
|
|
} else if (download.error) {
|
|
if (download.error.becauseBlockedByParentalControls) {
|
|
state = METADATA_STATE_BLOCKED_PARENTAL;
|
|
} else if (download.error.becauseBlockedByReputationCheck) {
|
|
state = METADATA_STATE_DIRTY;
|
|
} else {
|
|
state = METADATA_STATE_FAILED;
|
|
}
|
|
}
|
|
|
|
let metaData = { state, endTime: download.endTime };
|
|
if (download.succeeded) {
|
|
metaData.fileSize = download.target.size;
|
|
}
|
|
|
|
// The verdict may still be present even if the download succeeded.
|
|
if (download.error && download.error.reputationCheckVerdict) {
|
|
metaData.reputationCheckVerdict = download.error.reputationCheckVerdict;
|
|
}
|
|
|
|
// This should be executed before any async parts, to ensure the cache is
|
|
// updated before any notifications are activated.
|
|
await DownloadCache.setMetadata(download.source.url, metaData);
|
|
|
|
await this._updateHistoryListData(download.source.url);
|
|
},
|
|
|
|
async _updateHistoryListData(sourceUrl) {
|
|
for (let key of Object.getOwnPropertyNames(this._listPromises)) {
|
|
let downloadHistoryList = await this._listPromises[key];
|
|
downloadHistoryList.updateForMetaDataChange(
|
|
sourceUrl,
|
|
DownloadCache.get(sourceUrl)
|
|
);
|
|
}
|
|
},
|
|
};
|
|
|
|
/**
|
|
* This cache exists:
|
|
* - in order to optimize the load of DownloadsHistoryList, when Places
|
|
* annotations for history downloads must be read. In fact, annotations are
|
|
* stored in a single table, and reading all of them at once is much more
|
|
* efficient than an individual query.
|
|
* - to avoid needing to do asynchronous reading of the database during download
|
|
* list updates, which are designed to be synchronous (to improve UI
|
|
* responsiveness).
|
|
*
|
|
* The cache is initialized the first time DownloadHistory.getList is called, or
|
|
* when data is added.
|
|
*/
|
|
var DownloadCache = {
|
|
_data: new Map(),
|
|
_initializePromise: null,
|
|
|
|
/**
|
|
* Initializes the cache, loading the data from the places database.
|
|
*
|
|
* @return {Promise} Returns a promise that is resolved once the
|
|
* initialization is complete.
|
|
*/
|
|
ensureInitialized() {
|
|
if (this._initializePromise) {
|
|
return this._initializePromise;
|
|
}
|
|
this._initializePromise = (async () => {
|
|
PlacesUtils.history.addObserver(this, true);
|
|
|
|
let pageAnnos = await PlacesUtils.history.fetchAnnotatedPages([
|
|
METADATA_ANNO,
|
|
DESTINATIONFILEURI_ANNO,
|
|
]);
|
|
|
|
let metaDataPages = pageAnnos.get(METADATA_ANNO);
|
|
if (metaDataPages) {
|
|
for (let { uri, content } of metaDataPages) {
|
|
try {
|
|
this._data.set(uri.href, JSON.parse(content));
|
|
} catch (ex) {
|
|
// Do nothing - JSON.parse could throw.
|
|
}
|
|
}
|
|
}
|
|
|
|
let destinationFilePages = pageAnnos.get(DESTINATIONFILEURI_ANNO);
|
|
if (destinationFilePages) {
|
|
for (let { uri, content } of destinationFilePages) {
|
|
let newData = this.get(uri.href);
|
|
newData.targetFileSpec = content;
|
|
this._data.set(uri.href, newData);
|
|
}
|
|
}
|
|
})();
|
|
|
|
return this._initializePromise;
|
|
},
|
|
|
|
/**
|
|
* This returns an object containing the meta data for the supplied URL.
|
|
*
|
|
* @param {String} url The url to get the meta data for.
|
|
* @return {Object|null} Returns an empty object if there is no meta data found, or
|
|
* an object containing the meta data. The meta data
|
|
* will look like:
|
|
*
|
|
* { targetFileSpec, state, endTime, fileSize, ... }
|
|
*
|
|
* The targetFileSpec property is the value of "downloads/destinationFileURI",
|
|
* while the other properties are taken from "downloads/metaData". Any of the
|
|
* properties may be missing from the object.
|
|
*/
|
|
get(url) {
|
|
return this._data.get(url) || {};
|
|
},
|
|
|
|
/**
|
|
* Adds a download to the cache and the places database.
|
|
*
|
|
* @param {Download} download The download to add to the database and cache.
|
|
*/
|
|
async addDownload(download) {
|
|
await this.ensureInitialized();
|
|
|
|
let targetFile = new FileUtils.File(download.target.path);
|
|
let targetUri = Services.io.newFileURI(targetFile);
|
|
|
|
// This should be executed before any async parts, to ensure the cache is
|
|
// updated before any notifications are activated.
|
|
// Note: this intentionally overwrites any metadata as this is
|
|
// the start of a new download.
|
|
this._data.set(download.source.url, { targetFileSpec: targetUri.spec });
|
|
|
|
let originalPageInfo = await PlacesUtils.history.fetch(download.source.url);
|
|
|
|
let pageInfo = await PlacesUtils.history.insert({
|
|
url: download.source.url,
|
|
// 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.
|
|
title:
|
|
(originalPageInfo && originalPageInfo.title) || targetFile.leafName,
|
|
visits: [
|
|
{
|
|
// The start time is always available when we reach this point.
|
|
date: download.startTime,
|
|
transition: PlacesUtils.history.TRANSITIONS.DOWNLOAD,
|
|
referrer: download.source.referrerInfo
|
|
? download.source.referrerInfo.originalReferrer
|
|
: null,
|
|
},
|
|
],
|
|
});
|
|
|
|
await PlacesUtils.history.update({
|
|
annotations: new Map([["downloads/destinationFileURI", targetUri.spec]]),
|
|
// XXX Bug 1479445: We shouldn't have to supply both guid and url here,
|
|
// but currently we do.
|
|
guid: pageInfo.guid,
|
|
url: pageInfo.url,
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Sets the metadata for a given url. If the cache already contains meta data
|
|
* for the given url, it will be overwritten (note: the targetFileSpec will be
|
|
* maintained).
|
|
*
|
|
* @param {String} url The url to set the meta data for.
|
|
* @param {Object} metadata The new metaData to save in the cache.
|
|
*/
|
|
async setMetadata(url, metadata) {
|
|
await this.ensureInitialized();
|
|
|
|
// This should be executed before any async parts, to ensure the cache is
|
|
// updated before any notifications are activated.
|
|
let existingData = this.get(url);
|
|
let newData = { ...metadata };
|
|
if ("targetFileSpec" in existingData) {
|
|
newData.targetFileSpec = existingData.targetFileSpec;
|
|
}
|
|
this._data.set(url, newData);
|
|
|
|
try {
|
|
await PlacesUtils.history.update({
|
|
annotations: new Map([[METADATA_ANNO, JSON.stringify(metadata)]]),
|
|
url,
|
|
});
|
|
} catch (ex) {
|
|
Cu.reportError(ex);
|
|
}
|
|
},
|
|
|
|
QueryInterface: ChromeUtils.generateQI([
|
|
Ci.nsINavHistoryObserver,
|
|
Ci.nsISupportsWeakReference,
|
|
]),
|
|
|
|
// nsINavHistoryObserver
|
|
onDeleteURI(uri) {
|
|
this._data.delete(uri.spec);
|
|
},
|
|
onClearHistory() {
|
|
this._data.clear();
|
|
},
|
|
onBeginUpdateBatch() {},
|
|
onEndUpdateBatch() {},
|
|
onTitleChanged() {},
|
|
onFrecencyChanged() {},
|
|
onManyFrecenciesChanged() {},
|
|
onPageChanged() {},
|
|
onDeleteVisits() {},
|
|
};
|
|
|
|
/**
|
|
* Represents a download from the browser history. This object implements part
|
|
* of the interface of the Download object.
|
|
*
|
|
* While Download objects are shared between the public DownloadList and all the
|
|
* DownloadHistoryList instances, multiple HistoryDownload objects referring to
|
|
* the same item can be created for different DownloadHistoryList instances.
|
|
*
|
|
* @param placesNode
|
|
* The Places node from which the history download should be initialized.
|
|
*/
|
|
function HistoryDownload(placesNode) {
|
|
this.placesNode = placesNode;
|
|
|
|
// History downloads should get the referrer from Places (bug 829201).
|
|
this.source = {
|
|
url: placesNode.uri,
|
|
isPrivate: false,
|
|
};
|
|
this.target = {
|
|
path: undefined,
|
|
exists: false,
|
|
size: undefined,
|
|
};
|
|
|
|
// In case this download cannot obtain its end time from the Places metadata,
|
|
// use the time from the Places node, that is the start time of the download.
|
|
this.endTime = placesNode.time / 1000;
|
|
}
|
|
|
|
HistoryDownload.prototype = {
|
|
/**
|
|
* DownloadSlot containing this history download.
|
|
*/
|
|
slot: null,
|
|
|
|
/**
|
|
* Pushes information from Places metadata into this object.
|
|
*/
|
|
updateFromMetaData(metaData) {
|
|
try {
|
|
this.target.path = Cc["@mozilla.org/network/protocol;1?name=file"]
|
|
.getService(Ci.nsIFileProtocolHandler)
|
|
.getFileFromURLSpec(metaData.targetFileSpec).path;
|
|
} catch (ex) {
|
|
this.target.path = undefined;
|
|
}
|
|
|
|
if ("state" in metaData) {
|
|
this.succeeded = metaData.state == METADATA_STATE_FINISHED;
|
|
this.canceled =
|
|
metaData.state == METADATA_STATE_CANCELED ||
|
|
metaData.state == METADATA_STATE_PAUSED;
|
|
this.endTime = metaData.endTime;
|
|
|
|
// Recreate partial error information from the state saved in history.
|
|
if (metaData.state == METADATA_STATE_FAILED) {
|
|
this.error = { message: "History download failed." };
|
|
} else if (metaData.state == METADATA_STATE_BLOCKED_PARENTAL) {
|
|
this.error = { becauseBlockedByParentalControls: true };
|
|
} else if (metaData.state == METADATA_STATE_DIRTY) {
|
|
this.error = {
|
|
becauseBlockedByReputationCheck: true,
|
|
reputationCheckVerdict: metaData.reputationCheckVerdict || "",
|
|
};
|
|
} else {
|
|
this.error = null;
|
|
}
|
|
|
|
// Normal history downloads are assumed to exist until the user interface
|
|
// is refreshed, at which point these values may be updated.
|
|
this.target.exists = true;
|
|
this.target.size = metaData.fileSize;
|
|
} else {
|
|
// Metadata might be missing from a download that has started but hasn't
|
|
// stopped already. Normally, this state is overridden with the one from
|
|
// the corresponding in-progress session download. But if the browser is
|
|
// terminated abruptly and additionally the file with information about
|
|
// in-progress downloads is lost, we may end up using this state. We use
|
|
// the failed state to allow the download to be restarted.
|
|
//
|
|
// On the other hand, if the download is missing the target file
|
|
// annotation as well, it is just a very old one, and we can assume it
|
|
// succeeded.
|
|
this.succeeded = !this.target.path;
|
|
this.error = this.target.path ? { message: "Unstarted download." } : null;
|
|
this.canceled = false;
|
|
|
|
// These properties may be updated if the user interface is refreshed.
|
|
this.target.exists = false;
|
|
this.target.size = undefined;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* History downloads are never in progress.
|
|
*/
|
|
stopped: true,
|
|
|
|
/**
|
|
* No percentage indication is shown for history downloads.
|
|
*/
|
|
hasProgress: false,
|
|
|
|
/**
|
|
* History downloads cannot be restarted using their partial data, even if
|
|
* they are indicated as paused in their Places metadata. The only way is to
|
|
* use the information from a persisted session download, that will be shown
|
|
* instead of the history download. In case this session download is not
|
|
* available, we show the history download as canceled, not paused.
|
|
*/
|
|
hasPartialData: false,
|
|
|
|
/**
|
|
* This method may be called when deleting a history download.
|
|
*/
|
|
async finalize() {},
|
|
|
|
/**
|
|
* This method mimicks the "refresh" method of session downloads.
|
|
*/
|
|
async refresh() {
|
|
try {
|
|
this.target.size = (await OS.File.stat(this.target.path)).size;
|
|
this.target.exists = true;
|
|
} catch (ex) {
|
|
// We keep the known file size from the metadata, if any.
|
|
this.target.exists = false;
|
|
}
|
|
|
|
this.slot.list._notifyAllViews("onDownloadChanged", this);
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Represents one item in the list of public session and history downloads.
|
|
*
|
|
* The object may contain a session download, a history download, or both. When
|
|
* both a history and a session download are present, the session download gets
|
|
* priority and its information is accessed.
|
|
*
|
|
* @param list
|
|
* The DownloadHistoryList that owns this DownloadSlot object.
|
|
*/
|
|
function DownloadSlot(list) {
|
|
this.list = list;
|
|
}
|
|
|
|
DownloadSlot.prototype = {
|
|
list: null,
|
|
|
|
/**
|
|
* Download object representing the session download contained in this slot.
|
|
*/
|
|
sessionDownload: null,
|
|
|
|
/**
|
|
* HistoryDownload object contained in this slot.
|
|
*/
|
|
get historyDownload() {
|
|
return this._historyDownload;
|
|
},
|
|
set historyDownload(historyDownload) {
|
|
this._historyDownload = historyDownload;
|
|
if (historyDownload) {
|
|
historyDownload.slot = this;
|
|
}
|
|
},
|
|
_historyDownload: null,
|
|
|
|
/**
|
|
* Returns the Download or HistoryDownload object for displaying information
|
|
* and executing commands in the user interface.
|
|
*/
|
|
get download() {
|
|
return this.sessionDownload || this.historyDownload;
|
|
},
|
|
};
|
|
|
|
/**
|
|
* Represents an ordered collection of DownloadSlot objects containing a merged
|
|
* view on session downloads and history downloads. Views on this list will
|
|
* receive notifications for changes to both types of downloads.
|
|
*
|
|
* Downloads in this list are sorted from oldest to newest, with all session
|
|
* downloads after all the history downloads. When a new history download is
|
|
* added and the list also contains session downloads, the insertBefore option
|
|
* of the onDownloadAdded notification refers to the first session download.
|
|
*
|
|
* The list of downloads cannot be modified using the DownloadList methods.
|
|
*
|
|
* @param publicList
|
|
* Underlying DownloadList containing public downloads.
|
|
* @param place
|
|
* Places query used to retrieve history downloads.
|
|
*/
|
|
var DownloadHistoryList = function(publicList, place) {
|
|
DownloadList.call(this);
|
|
|
|
// While "this._slots" contains all the data in order, the other properties
|
|
// provide fast access for the most common operations.
|
|
this._slots = [];
|
|
this._slotsForUrl = new Map();
|
|
this._slotForDownload = new WeakMap();
|
|
|
|
// Start the asynchronous queries to retrieve history and session downloads.
|
|
publicList.addView(this).catch(Cu.reportError);
|
|
let query = {},
|
|
options = {};
|
|
PlacesUtils.history.queryStringToQuery(place, query, options);
|
|
|
|
// NB: The addObserver call sets our nsINavHistoryResultObserver.result.
|
|
let result = PlacesUtils.history.executeQuery(query.value, options.value);
|
|
result.addObserver(this);
|
|
|
|
// Our history result observer is long lived for fast shared views, so free
|
|
// the reference on shutdown to prevent leaks.
|
|
Services.obs.addObserver(() => {
|
|
this.result = null;
|
|
}, "quit-application-granted");
|
|
};
|
|
|
|
this.DownloadHistoryList.prototype = {
|
|
__proto__: DownloadList.prototype,
|
|
|
|
/**
|
|
* This is set when executing the Places query.
|
|
*/
|
|
get result() {
|
|
return this._result;
|
|
},
|
|
set result(result) {
|
|
if (this._result == result) {
|
|
return;
|
|
}
|
|
|
|
if (this._result) {
|
|
this._result.removeObserver(this);
|
|
this._result.root.containerOpen = false;
|
|
}
|
|
|
|
this._result = result;
|
|
|
|
if (this._result) {
|
|
this._result.root.containerOpen = true;
|
|
}
|
|
},
|
|
_result: null,
|
|
|
|
/**
|
|
* Updates the download history item when the meta data or destination file
|
|
* changes.
|
|
*
|
|
* @param {String} sourceUrl The sourceUrl which was updated.
|
|
* @param {Object} metaData The new meta data for the sourceUrl.
|
|
*/
|
|
updateForMetaDataChange(sourceUrl, metaData) {
|
|
let slotsForUrl = this._slotsForUrl.get(sourceUrl);
|
|
if (!slotsForUrl) {
|
|
return;
|
|
}
|
|
|
|
for (let slot of slotsForUrl) {
|
|
if (slot.sessionDownload) {
|
|
// The visible data doesn't change, so we don't have to notify views.
|
|
return;
|
|
}
|
|
slot.historyDownload.updateFromMetaData(metaData);
|
|
this._notifyAllViews("onDownloadChanged", slot.download);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Index of the first slot that contains a session download. This is equal to
|
|
* the length of the list when there are no session downloads.
|
|
*/
|
|
_firstSessionSlotIndex: 0,
|
|
|
|
_insertSlot({ slot, index, slotsForUrl }) {
|
|
// Add the slot to the ordered array.
|
|
this._slots.splice(index, 0, slot);
|
|
this._downloads.splice(index, 0, slot.download);
|
|
if (!slot.sessionDownload) {
|
|
this._firstSessionSlotIndex++;
|
|
}
|
|
|
|
// Add the slot to the fast access maps.
|
|
slotsForUrl.add(slot);
|
|
this._slotsForUrl.set(slot.download.source.url, slotsForUrl);
|
|
|
|
// Add the associated view items.
|
|
this._notifyAllViews("onDownloadAdded", slot.download, {
|
|
insertBefore: this._downloads[index + 1],
|
|
});
|
|
},
|
|
|
|
_removeSlot({ slot, slotsForUrl }) {
|
|
// Remove the slot from the ordered array.
|
|
let index = this._slots.indexOf(slot);
|
|
this._slots.splice(index, 1);
|
|
this._downloads.splice(index, 1);
|
|
if (this._firstSessionSlotIndex > index) {
|
|
this._firstSessionSlotIndex--;
|
|
}
|
|
|
|
// Remove the slot from the fast access maps.
|
|
slotsForUrl.delete(slot);
|
|
if (slotsForUrl.size == 0) {
|
|
this._slotsForUrl.delete(slot.download.source.url);
|
|
}
|
|
|
|
// Remove the associated view items.
|
|
this._notifyAllViews("onDownloadRemoved", slot.download);
|
|
},
|
|
|
|
/**
|
|
* Ensures that the information about a history download is stored in at least
|
|
* one slot, adding a new one at the end of the list if necessary.
|
|
*
|
|
* A reference to the same Places node will be stored in the HistoryDownload
|
|
* object for all the DownloadSlot objects associated with the source URL.
|
|
*
|
|
* @param placesNode
|
|
* The Places node that represents the history download.
|
|
*/
|
|
_insertPlacesNode(placesNode) {
|
|
let slotsForUrl = this._slotsForUrl.get(placesNode.uri) || new Set();
|
|
|
|
// If there are existing slots associated with this URL, we only have to
|
|
// ensure that the Places node reference is kept updated in case the more
|
|
// recent Places notification contained a different node object.
|
|
if (slotsForUrl.size > 0) {
|
|
for (let slot of slotsForUrl) {
|
|
if (!slot.historyDownload) {
|
|
slot.historyDownload = new HistoryDownload(placesNode);
|
|
} else {
|
|
slot.historyDownload.placesNode = placesNode;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// If there are no existing slots for this URL, we have to create a new one.
|
|
// Since the history download is visible in the slot, we also have to update
|
|
// the object using the Places metadata.
|
|
let historyDownload = new HistoryDownload(placesNode);
|
|
historyDownload.updateFromMetaData(DownloadCache.get(placesNode.uri));
|
|
let slot = new DownloadSlot(this);
|
|
slot.historyDownload = historyDownload;
|
|
this._insertSlot({ slot, slotsForUrl, index: this._firstSessionSlotIndex });
|
|
},
|
|
|
|
// nsINavHistoryResultObserver
|
|
containerStateChanged(node, oldState, newState) {
|
|
this.invalidateContainer(node);
|
|
},
|
|
|
|
// nsINavHistoryResultObserver
|
|
invalidateContainer(container) {
|
|
this._notifyAllViews("onDownloadBatchStarting");
|
|
|
|
// Remove all the current slots containing only history downloads.
|
|
for (let index = this._slots.length - 1; index >= 0; index--) {
|
|
let slot = this._slots[index];
|
|
if (slot.sessionDownload) {
|
|
// The visible data doesn't change, so we don't have to notify views.
|
|
slot.historyDownload = null;
|
|
} else {
|
|
let slotsForUrl = this._slotsForUrl.get(slot.download.source.url);
|
|
this._removeSlot({ slot, slotsForUrl });
|
|
}
|
|
}
|
|
|
|
// Add new slots or reuse existing ones for history downloads.
|
|
for (let index = container.childCount - 1; index >= 0; --index) {
|
|
try {
|
|
this._insertPlacesNode(container.getChild(index));
|
|
} catch (ex) {
|
|
Cu.reportError(ex);
|
|
}
|
|
}
|
|
|
|
this._notifyAllViews("onDownloadBatchEnded");
|
|
},
|
|
|
|
// nsINavHistoryResultObserver
|
|
nodeInserted(parent, placesNode) {
|
|
this._insertPlacesNode(placesNode);
|
|
},
|
|
|
|
// nsINavHistoryResultObserver
|
|
nodeRemoved(parent, placesNode, aOldIndex) {
|
|
let slotsForUrl = this._slotsForUrl.get(placesNode.uri);
|
|
for (let slot of slotsForUrl) {
|
|
if (slot.sessionDownload) {
|
|
// The visible data doesn't change, so we don't have to notify views.
|
|
slot.historyDownload = null;
|
|
} else {
|
|
this._removeSlot({ slot, slotsForUrl });
|
|
}
|
|
}
|
|
},
|
|
|
|
// nsINavHistoryResultObserver
|
|
nodeIconChanged() {},
|
|
nodeTitleChanged() {},
|
|
nodeKeywordChanged() {},
|
|
nodeDateAddedChanged() {},
|
|
nodeLastModifiedChanged() {},
|
|
nodeHistoryDetailsChanged() {},
|
|
nodeTagsChanged() {},
|
|
sortingChanged() {},
|
|
nodeMoved() {},
|
|
nodeURIChanged() {},
|
|
batching() {},
|
|
|
|
// DownloadList callback
|
|
onDownloadAdded(download) {
|
|
let url = download.source.url;
|
|
let slotsForUrl = this._slotsForUrl.get(url) || new Set();
|
|
|
|
// For every source URL, there can be at most one slot containing a history
|
|
// download without an associated session download. If we find one, then we
|
|
// can reuse it for the current session download, although we have to move
|
|
// it together with the other session downloads.
|
|
let slot = [...slotsForUrl][0];
|
|
if (slot && !slot.sessionDownload) {
|
|
// Remove the slot because we have to change its position.
|
|
this._removeSlot({ slot, slotsForUrl });
|
|
} else {
|
|
slot = new DownloadSlot(this);
|
|
}
|
|
slot.sessionDownload = download;
|
|
this._insertSlot({ slot, slotsForUrl, index: this._slots.length });
|
|
this._slotForDownload.set(download, slot);
|
|
},
|
|
|
|
// DownloadList callback
|
|
onDownloadChanged(download) {
|
|
let slot = this._slotForDownload.get(download);
|
|
this._notifyAllViews("onDownloadChanged", slot.download);
|
|
},
|
|
|
|
// DownloadList callback
|
|
onDownloadRemoved(download) {
|
|
let url = download.source.url;
|
|
let slotsForUrl = this._slotsForUrl.get(url);
|
|
let slot = this._slotForDownload.get(download);
|
|
this._removeSlot({ slot, slotsForUrl });
|
|
|
|
this._slotForDownload.delete(download);
|
|
|
|
// If there was only one slot for this source URL and it also contained a
|
|
// history download, we should resurrect it in the correct area of the list.
|
|
if (slotsForUrl.size == 0 && slot.historyDownload) {
|
|
// We have one download slot containing both a session download and a
|
|
// history download, and we are now removing the session download.
|
|
// Previously, we did not use the Places metadata because it was obscured
|
|
// by the session download. Since this is no longer the case, we have to
|
|
// read the latest metadata before resurrecting the history download.
|
|
slot.historyDownload.updateFromMetaData(DownloadCache.get(url));
|
|
slot.sessionDownload = null;
|
|
// Place the resurrected history slot after all the session slots.
|
|
this._insertSlot({
|
|
slot,
|
|
slotsForUrl,
|
|
index: this._firstSessionSlotIndex,
|
|
});
|
|
}
|
|
},
|
|
|
|
// DownloadList
|
|
add() {
|
|
throw new Error("Not implemented.");
|
|
},
|
|
|
|
// DownloadList
|
|
remove() {
|
|
throw new Error("Not implemented.");
|
|
},
|
|
|
|
// DownloadList
|
|
removeFinished() {
|
|
throw new Error("Not implemented.");
|
|
},
|
|
};
|