Revert the backout of bug 938023 (changesets 19fbd3fb0373:8146150d4df8)

This commit is contained in:
Wes Kocher 2013-12-20 13:43:46 -08:00
parent ddbcbb1b36
commit 1221083b0e
29 changed files with 1613 additions and 2 deletions

View File

@ -860,6 +860,10 @@ pref("b2g.neterror.url", "app://system.gaiamobile.org/net_error.html");
// Enable Web Speech synthesis API
pref("media.webspeech.synth.enabled", true);
// Downloads API
pref("dom.mozDownloads.enabled", true);
pref("dom.downloads.max_retention_days", 7);
// Downloads API
pref("dom.mozDownloads.enabled", true);

View File

@ -26,6 +26,8 @@ Cu.import('resource://gre/modules/SignInToWebsite.jsm');
SignInToWebsiteController.init();
Cu.import('resource://gre/modules/FxAccountsMgmtService.jsm');
Cu.import('resource://gre/modules/DownloadsAPI.jsm');
XPCOMUtils.defineLazyServiceGetter(Services, 'env',
'@mozilla.org/process/environment;1',
'nsIEnvironment');
@ -1495,3 +1497,21 @@ Services.obs.addObserver(function resetProfile(subject, topic, data) {
.getService(Ci.nsIAppStartup);
appStartup.quit(Ci.nsIAppStartup.eForceQuit);
}, 'b2g-reset-profile', false);
/**
* CID of our implementation of nsIDownloadManagerUI.
*/
const kTransferCid = Components.ID("{1b4c85df-cbdd-4bb6-b04e-613caece083c}");
/**
* Contract ID of the service implementing nsITransfer.
*/
const kTransferContractId = "@mozilla.org/transfer;1";
// Override Toolkit's nsITransfer implementation with the one from the
// JavaScript API for downloads. This will eventually be removed when
// nsIDownloadManager will not be available anymore (bug 851471). The
// old code in this module will be removed in bug 899110.
Components.manager.QueryInterface(Ci.nsIComponentRegistrar)
.registerFactory(kTransferCid, "",
kTransferContractId, null);

View File

@ -60,3 +60,5 @@ if test "$OS_TARGET" = "Android"; then
MOZ_NUWA_PROCESS=
fi
MOZ_FOLD_LIBS=1
MOZ_JSDOWNLOADS=1

View File

@ -393,6 +393,8 @@
@BINPATH@/components/jsconsole-clhandler.js
@BINPATH@/components/nsDownloadManagerUI.manifest
@BINPATH@/components/nsDownloadManagerUI.js
@BINPATH@/components/Downloads.manifest
@BINPATH@/components/DownloadLegacy.js
@BINPATH@/components/nsSidebar.manifest
@BINPATH@/components/nsSidebar.js
@ -562,6 +564,9 @@
@BINPATH@/components/PaymentRequestInfo.js
@BINPATH@/components/Payment.manifest
@BINPATH@/components/DownloadsAPI.js
@BINPATH@/components/DownloadsAPI.manifest
; InputMethod API
@BINPATH@/components/MozKeyboard.js
@BINPATH@/components/InputMethod.manifest
@ -786,6 +791,7 @@ bin/components/@DLL_PREFIX@nkgnomevfs@DLL_SUFFIX@
@BINPATH@/components/FilePicker.js
@BINPATH@/components/FxAccountsUIGlue.js
@BINPATH@/components/HelperAppDialog.js
@BINPATH@/components/DownloadsUI.js
@BINPATH@/components/DataStore.manifest
@BINPATH@/components/DataStoreService.js

View File

@ -128,6 +128,10 @@ const kEventConstructors = {
return new DeviceStorageChangeEvent(aName, aProps);
},
},
DownloadEvent: { create: function (aName, aProps) {
return new DownloadEvent(aName, aProps);
},
},
DOMTransactionEvent: { create: function (aName, aProps) {
return new DOMTransactionEvent(aName, aProps);
},

View File

@ -318,6 +318,11 @@ this.PermissionsTable = { geolocation: {
privileged: ALLOW_ACTION,
certified: ALLOW_ACTION
},
"downloads": {
app: DENY_ACTION,
privileged: DENY_ACTION,
certified: ALLOW_ACTION
},
};
/**

View File

@ -184,12 +184,13 @@ DOMRequestIpcHelper.prototype = {
this._listeners = null;
this._requests = null;
this._window = null;
// Objects inheriting from DOMRequestIPCHelper may have an uninit function.
if (this.uninit) {
this.uninit();
}
this._window = null;
},
observe: function(aSubject, aTopic, aData) {

View File

@ -1539,6 +1539,13 @@ Navigator::DoNewResolve(JSContext* aCx, JS::Handle<JSObject*> aObject,
}
}
if (name.EqualsLiteral("mozDownloadManager")) {
if (!CheckPermission("downloads")) {
aValue.setNull();
return true;
}
}
domObject = construct(aCx, naviObj);
if (!domObject) {
return Throw(aCx, NS_ERROR_FAILURE);

10
dom/downloads/moz.build Normal file
View File

@ -0,0 +1,10 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
if CONFIG["MOZ_B2G"]:
TEST_DIRS += ['tests']
PARALLEL_DIRS += ['src']

View File

@ -0,0 +1,320 @@
/* 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/. */
"use strict";
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/DOMRequestHelper.jsm");
Cu.import("resource://gre/modules/DownloadsIPC.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
"@mozilla.org/childprocessmessagemanager;1",
"nsIMessageSender");
function debug(aStr) {
dump("-*- DownloadsAPI.js : " + aStr + "\n");
}
function DOMDownloadManagerImpl() {
debug("DOMDownloadManagerImpl constructor");
}
DOMDownloadManagerImpl.prototype = {
__proto__: DOMRequestIpcHelper.prototype,
// nsIDOMGlobalPropertyInitializer implementation
init: function(aWindow) {
debug("DownloadsManager init");
this.initDOMRequestHelper(aWindow,
["Downloads:Added",
"Downloads:Removed"]);
},
uninit: function() {
debug("uninit");
downloadsCache.evict(this._window);
},
set ondownloadstart(aHandler) {
this.__DOM_IMPL__.setEventHandler("ondownloadstart", aHandler);
},
get ondownloadstart() {
return this.__DOM_IMPL__.getEventHandler("ondownloadstart");
},
getDownloads: function() {
debug("getDownloads()");
return this.createPromise(function (aResolve, aReject) {
DownloadsIPC.getDownloads().then(
function(aDownloads) {
// Turn the list of download objects into DOM objects and
// send them.
let array = Cu.createArrayIn(this._window);
for (let id in aDownloads) {
let dom = createDOMDownloadObject(this._window, aDownloads[id]);
array.push(this._prepareForContent(dom));
}
aResolve(array);
}.bind(this),
function() {
aReject("GetDownloadsError");
}
);
}.bind(this));
},
clearAllDone: function() {
debug("clearAllDone()");
return this.createPromise(function (aResolve, aReject) {
DownloadsIPC.clearAllDone().then(
function(aDownloads) {
// Turn the list of download objects into DOM objects and
// send them.
let array = Cu.createArrayIn(this._window);
for (let id in aDownloads) {
let dom = createDOMDownloadObject(this._window, aDownloads[id]);
array.push(this._prepareForContent(dom));
}
aResolve(array);
}.bind(this),
function() {
aReject("ClearAllDoneError");
}
);
}.bind(this));
},
remove: function(aDownload) {
debug("remove " + aDownload.url + " " + aDownload.id);
return this.createPromise(function (aResolve, aReject) {
if (!downloadsCache.has(this._window, aDownload.id)) {
debug("no download " + aDownload.id);
aReject("InvalidDownload");
return;
}
DownloadsIPC.remove(aDownload.id).then(
function(aResult) {
let dom = createDOMDownloadObject(this._window, aResult);
// Change the state right away to not race against the update message.
dom.wrappedJSObject.state = "finalized";
aResolve(this._prepareForContent(dom));
}.bind(this),
function() {
aReject("RemoveError");
}
);
}.bind(this));
},
/**
* Turns a chrome download object into a content accessible one.
* When we have __DOM_IMPL__ available we just use that, otherwise
* we run _create() with the wrapped js object.
*/
_prepareForContent: function(aChromeObject) {
if (aChromeObject.__DOM_IMPL__) {
return aChromeObject.__DOM_IMPL__;
}
let res = this._window.DOMDownload._create(this._window,
aChromeObject.wrappedJSObject);
return res;
},
receiveMessage: function(aMessage) {
let data = aMessage.data;
switch(aMessage.name) {
case "Downloads:Added":
debug("Adding " + uneval(data));
let event = new this._window.DownloadEvent("downloadstart", {
download:
this._prepareForContent(createDOMDownloadObject(this._window, data))
});
this.__DOM_IMPL__.dispatchEvent(event);
break;
}
},
classID: Components.ID("{c6587afa-0696-469f-9eff-9dac0dd727fe}"),
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
Ci.nsISupportsWeakReference,
Ci.nsIObserver,
Ci.nsIDOMGlobalPropertyInitializer]),
};
/**
* Keep track of download objects per window.
*/
let downloadsCache = {
init: function() {
this.cache = new WeakMap();
},
has: function(aWindow, aId) {
let downloads = this.cache.get(aWindow);
return !!(downloads && downloads[aId]);
},
get: function(aWindow, aDownload) {
let downloads = this.cache.get(aWindow);
if (!(downloads && downloads[aDownload.id])) {
debug("Adding download " + aDownload.id + " to cache.");
if (!downloads) {
this.cache.set(aWindow, {});
downloads = this.cache.get(aWindow);
}
// Create the object and add it to the cache.
let impl = Cc["@mozilla.org/downloads/download;1"]
.createInstance(Ci.nsISupports);
impl.wrappedJSObject._init(aWindow, aDownload);
downloads[aDownload.id] = impl;
}
return downloads[aDownload.id];
},
evict: function(aWindow) {
this.cache.delete(aWindow);
}
};
downloadsCache.init();
/**
* The DOM facade of a download object.
*/
function createDOMDownloadObject(aWindow, aDownload) {
return downloadsCache.get(aWindow, aDownload);
}
function DOMDownloadImpl() {
debug("DOMDownloadImpl constructor ");
this.wrappedJSObject = this;
this.totalBytes = 0;
this.currentBytes = 0;
this.url = null;
this.path = null;
this.state = "stopped";
this.contentType = null;
this.startTime = Date.now();
this.error = null;
/* private fields */
this.id = null;
}
DOMDownloadImpl.prototype = {
createPromise: function(aPromiseInit) {
return new this._window.Promise(aPromiseInit);
},
pause: function() {
debug("DOMDownloadImpl pause");
let id = this.id;
// We need to wrap the Promise.jsm promise in a "real" DOM promise...
return this.createPromise(function(aResolve, aReject) {
DownloadsIPC.pause(id).then(aResolve, aReject);
});
},
resume: function() {
debug("DOMDownloadImpl resume");
let id = this.id;
// We need to wrap the Promise.jsm promise in a "real" DOM promise...
return this.createPromise(function(aResolve, aReject) {
DownloadsIPC.resume(id).then(aResolve, aReject);
});
},
set onstatechange(aHandler) {
this.__DOM_IMPL__.setEventHandler("onstatechange", aHandler);
},
get onstatechange() {
return this.__DOM_IMPL__.getEventHandler("onstatechange");
},
_init: function(aWindow, aDownload) {
this._window = aWindow;
this.id = aDownload.id;
this._update(aDownload);
Services.obs.addObserver(this, "downloads-state-change-" + this.id,
/* ownsWeak */ true);
debug("observer set for " + this.id);
},
/**
* Updates the state of the object and fires the statechange event.
*/
_update: function(aDownload) {
debug("update " + uneval(aDownload));
if (this.id != aDownload.id) {
return;
}
let props = ["totalBytes", "currentBytes", "url", "path", "state",
"contentType", "startTime"];
let changed = false;
props.forEach((prop) => {
if (aDownload[prop] && (aDownload[prop] != this[prop])) {
this[prop] = aDownload[prop];
changed = true;
}
});
if (aDownload.error) {
this.error = new this._window.DOMError("DownloadError", aDownload.error);
} else {
this.error = null;
}
// The visible state has not changed, so no need to fire an event.
if (!changed) {
return;
}
// __DOM_IMPL__ may not be available at first update.
if (this.__DOM_IMPL__) {
let event = new this._window.DownloadEvent("statechange", {
download: this.__DOM_IMPL__
});
debug("Dispatching statechange event. state=" + this.state);
this.__DOM_IMPL__.dispatchEvent(event);
}
},
observe: function(aSubject, aTopic, aData) {
debug("DOMDownloadImpl observe " + aTopic);
if (aTopic !== "downloads-state-change-" + this.id) {
return;
}
try {
let download = JSON.parse(aData);
// We get the start time as milliseconds, not as a Date object.
if (download.startTime) {
download.startTime = new Date(download.startTime);
}
this._update(download);
} catch(e) {}
},
classID: Components.ID("{96b81b99-aa96-439d-8c59-92eeed34705f}"),
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
Ci.nsIObserver,
Ci.nsISupportsWeakReference])
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DOMDownloadManagerImpl,
DOMDownloadImpl]);

View File

@ -0,0 +1,255 @@
/* 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/. */
"use strict";
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
this.EXPORTED_SYMBOLS = [];
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Downloads.jsm");
Cu.import("resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
"@mozilla.org/parentprocessmessagemanager;1",
"nsIMessageBroadcaster");
function debug(aStr) {
dump("-*- DownloadsAPI.jsm : " + aStr + "\n");
}
function sendPromiseMessage(aMm, aMessageName, aData, aError) {
debug("sendPromiseMessage " + aMessageName);
let msg = {
id: aData.id,
promiseId: aData.promiseId
};
if (aError) {
msg.error = aError;
}
aMm.sendAsyncMessage(aMessageName, msg);
}
let DownloadsAPI = {
init: function() {
debug("init");
this._ids = new WeakMap(); // Maps toolkit download objects to ids.
this._index = {}; // Maps ids to downloads.
["Downloads:GetList",
"Downloads:ClearAllDone",
"Downloads:Remove",
"Downloads:Pause",
"Downloads:Resume"].forEach((msgName) => {
ppmm.addMessageListener(msgName, this);
});
let self = this;
Task.spawn(function () {
let list = yield Downloads.getList(Downloads.ALL);
yield list.addView(self);
debug("view added to download list.");
}).then(null, Components.utils.reportError);
this._currentId = 0;
},
/**
* Returns a unique id for each download, hashing the url and the path.
*/
downloadId: function(aDownload) {
let id = this._ids.get(aDownload, null);
if (!id) {
id = "download-" + this._currentId++;
this._ids.set(aDownload, id);
this._index[id] = aDownload;
}
return id;
},
getDownloadById: function(aId) {
return this._index[aId];
},
/**
* Converts a download object into a plain json object that we'll
* send to the DOM side.
*/
jsonDownload: function(aDownload) {
let res = {
totalBytes: aDownload.totalBytes,
currentBytes: aDownload.currentBytes,
url: aDownload.source.url,
path: aDownload.target.path,
contentType: aDownload.contentType,
startTime: aDownload.startTime.getTime()
};
if (aDownload.error) {
res.error = aDownload.error.name;
}
res.id = this.downloadId(aDownload);
// The state of the download. Can be any of "downloading", "stopped",
// "succeeded", finalized".
// Default to "stopped"
res.state = "stopped";
if (!aDownload.stopped &&
!aDownload.canceled &&
!aDownload.succeeded &&
!aDownload.DownloadError) {
res.state = "downloading";
} else if (aDownload.succeeded) {
res.state = "succeeded";
}
return res;
},
/**
* download view methods.
*/
onDownloadAdded: function(aDownload) {
let download = this.jsonDownload(aDownload);
debug("onDownloadAdded " + uneval(download));
ppmm.broadcastAsyncMessage("Downloads:Added", download);
},
onDownloadRemoved: function(aDownload) {
let download = this.jsonDownload(aDownload);
download.state = "finalized";
debug("onDownloadRemoved " + uneval(download));
ppmm.broadcastAsyncMessage("Downloads:Removed", download);
this._index[this._ids.get(aDownload)] = null;
this._ids.delete(aDownload);
},
onDownloadChanged: function(aDownload) {
let download = this.jsonDownload(aDownload);
debug("onDownloadChanged " + uneval(download));
ppmm.broadcastAsyncMessage("Downloads:Changed", download);
},
receiveMessage: function(aMessage) {
if (!aMessage.target.assertPermission("downloads")) {
debug("No 'downloads' permission!");
return;
}
debug("message: " + aMessage.name);
// Removing 'Downloads:' and turning first letter to lower case to
// build the function name from the message name.
let c = aMessage.name[10].toLowerCase();
let methodName = c + aMessage.name.substring(11);
if (this[methodName] && typeof this[methodName] === "function") {
this[methodName](aMessage.data, aMessage.target);
} else {
debug("Unimplemented method: " + methodName);
}
},
getList: function(aData, aMm) {
debug("getList called!");
let self = this;
Task.spawn(function () {
let list = yield Downloads.getList(Downloads.ALL);
let downloads = yield list.getAll();
let res = [];
downloads.forEach((aDownload) => {
res.push(self.jsonDownload(aDownload));
});
aMm.sendAsyncMessage("Downloads:GetList:Return", res);
}).then(null, Components.utils.reportError);
},
clearAllDone: function(aData, aMm) {
debug("clearAllDone called!");
let self = this;
Task.spawn(function () {
let list = yield Downloads.getList(Downloads.ALL);
yield list.removeFinished();
list = yield Downloads.getList(Downloads.ALL);
let downloads = yield list.getAll();
let res = [];
downloads.forEach((aDownload) => {
res.push(self.jsonDownload(aDownload));
});
aMm.sendAsyncMessage("Downloads:ClearAllDone:Return", res);
}).then(null, Components.utils.reportError);
},
remove: function(aData, aMm) {
debug("remove id " + aData.id);
let download = this.getDownloadById(aData.id);
if (!download) {
sendPromiseMessage(aMm, "Downloads:Remove:Return",
aData, "NoSuchDownload");
return;
}
Task.spawn(function() {
yield download.finalize(true);
let list = yield Downloads.getList(Downloads.ALL);
yield list.remove(download);
}).then(
function() {
sendPromiseMessage(aMm, "Downloads:Remove:Return", aData);
},
function() {
sendPromiseMessage(aMm, "Downloads:Remove:Return",
aData, "RemoveError");
}
);
},
pause: function(aData, aMm) {
debug("pause id " + aData.id);
let download = this.getDownloadById(aData.id);
if (!download) {
sendPromiseMessage(aMm, "Downloads:Pause:Return",
aData, "NoSuchDownload");
return;
}
download.cancel().then(
function() {
sendPromiseMessage(aMm, "Downloads:Pause:Return", aData);
},
function() {
sendPromiseMessage(aMm, "Downloads:Pause:Return",
aData, "PauseError");
}
);
},
resume: function(aData, aMm) {
debug("resume id " + aData.id);
let download = this.getDownloadById(aData.id);
if (!download) {
sendPromiseMessage(aMm, "Downloads:Resume:Return",
aData, "NoSuchDownload");
return;
}
download.start().then(
function() {
sendPromiseMessage(aMm, "Downloads:Resume:Return", aData);
},
function() {
sendPromiseMessage(aMm, "Downloads:Resume:Return",
aData, "ResumeError");
}
);
}
};
DownloadsAPI.init();

View File

@ -0,0 +1,6 @@
# DownloadsAPI.js
component {c6587afa-0696-469f-9eff-9dac0dd727fe} DownloadsAPI.js
contract @mozilla.org/downloads/manager;1 {c6587afa-0696-469f-9eff-9dac0dd727fe}
component {96b81b99-aa96-439d-8c59-92eeed34705f} DownloadsAPI.js
contract @mozilla.org/downloads/download;1 {96b81b99-aa96-439d-8c59-92eeed34705f}

View File

@ -0,0 +1,221 @@
/* 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/. */
"use strict";
const Cc = Components.classes;
const Ci = Components.interfaces;
const Cu = Components.utils;
this.EXPORTED_SYMBOLS = ["DownloadsIPC"];
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
"@mozilla.org/childprocessmessagemanager;1",
"nsIMessageSender");
/**
* This module lives in the child process and receives the ipc messages
* from the parent. It saves the download's state and redispatch changes
* to DOM objects using an observer notification.
*
* This module needs to be loaded once and only once per process.
*/
function debug(aStr) {
dump("-*- DownloadsIPC.jsm : " + aStr + "\n");
}
const ipcMessages = ["Downloads:Added",
"Downloads:Removed",
"Downloads:Changed",
"Downloads:GetList:Return",
"Downloads:ClearAllDone:Return",
"Downloads:Remove:Return",
"Downloads:Pause:Return",
"Downloads:Resume:Return"];
this.DownloadsIPC = {
downloads: {},
init: function() {
debug("init");
Services.obs.addObserver(this, "xpcom-shutdown", false);
ipcMessages.forEach((aMessage) => {
cpmm.addMessageListener(aMessage, this);
});
// We need to get the list of current downloads.
this.ready = false;
this.getListPromises = [];
this.clearAllPromises = [];
this.downloadPromises = {};
cpmm.sendAsyncMessage("Downloads:GetList", {});
this._promiseId = 0;
},
notifyChanges: function(aId) {
// TODO: use the subject instead of stringifying.
if (this.downloads[aId]) {
debug("notifyChanges notifying changes for " + aId);
Services.obs.notifyObservers(null, "downloads-state-change-" + aId,
JSON.stringify(this.downloads[aId]));
} else {
debug("notifyChanges failed for " + aId)
}
},
_updateDownloadsArray: function(aDownloads) {
this.downloads = [];
// We actually have an array of downloads.
aDownloads.forEach((aDownload) => {
this.downloads[aDownload.id] = aDownload;
});
},
receiveMessage: function(aMessage) {
let download = aMessage.data;
debug("message: " + aMessage.name + " " + download.id);
switch(aMessage.name) {
case "Downloads:GetList:Return":
this._updateDownloadsArray(download);
if (!this.ready) {
this.getListPromises.forEach(aPromise =>
aPromise.resolve(this.downloads));
this.getListPromises.length = 0;
}
this.ready = true;
break;
case "Downloads:ClearAllDone:Return":
this._updateDownloadsArray(download);
this.clearAllPromises.forEach(aPromise =>
aPromise.resolve(this.downloads));
this.clearAllPromises.length = 0;
break;
case "Downloads:Added":
this.downloads[download.id] = download;
this.notifyChanges(download.id);
break;
case "Downloads:Removed":
if (this.downloads[download.id]) {
this.downloads[download.id] = download;
this.notifyChanges(download.id);
delete this.downloads[download.id];
}
break;
case "Downloads:Changed":
// Only update properties that actually changed.
let cached = this.downloads[download.id];
if (!cached) {
debug("No download found for " + download.id);
return;
}
let props = ["totalBytes", "currentBytes", "url", "path", "state",
"contentType", "startTime"];
let changed = false;
props.forEach((aProp) => {
if (download[aProp] && (download[aProp] != cached[aProp])) {
cached[aProp] = download[aProp];
changed = true;
}
});
// Updating the error property. We always get a 'state' change as
// well.
cached.error = download.error;
if (changed) {
this.notifyChanges(download.id);
}
break;
case "Downloads:Remove:Return":
case "Downloads:Pause:Return":
case "Downloads:Resume:Return":
if (this.downloadPromises[download.promiseId]) {
if (!download.error) {
this.downloadPromises[download.promiseId].resolve(download);
} else {
this.downloadPromises[download.promiseId].reject(download);
}
delete this.downloadPromises[download.promiseId];
}
break;
}
},
/**
* Returns a promise that is resolved with the list of current downloads.
*/
getDownloads: function() {
debug("getDownloads()");
let deferred = Promise.defer();
if (this.ready) {
debug("Returning existing list.");
deferred.resolve(this.downloads);
} else {
this.getListPromises.push(deferred);
}
return deferred.promise;
},
/**
* Returns a promise that is resolved with the list of current downloads.
*/
clearAllDone: function() {
debug("clearAllDone");
let deferred = Promise.defer();
this.clearAllPromises.push(deferred);
cpmm.sendAsyncMessage("Downloads:ClearAllDone", {});
return deferred.promise;
},
promiseId: function() {
return this._promiseId++;
},
remove: function(aId) {
debug("remove " + aId);
let deferred = Promise.defer();
let pId = this.promiseId();
this.downloadPromises[pId] = deferred;
cpmm.sendAsyncMessage("Downloads:Remove",
{ id: aId, promiseId: pId });
return deferred.promise;
},
pause: function(aId) {
debug("pause " + aId);
let deferred = Promise.defer();
let pId = this.promiseId();
this.downloadPromises[pId] = deferred;
cpmm.sendAsyncMessage("Downloads:Pause",
{ id: aId, promiseId: pId });
return deferred.promise;
},
resume: function(aId) {
debug("resume " + aId);
let deferred = Promise.defer();
let pId = this.promiseId();
this.downloadPromises[pId] = deferred;
cpmm.sendAsyncMessage("Downloads:Resume",
{ id: aId, promiseId: pId });
return deferred.promise;
},
observe: function(aSubject, aTopic, aData) {
if (aTopic == "xpcom-shutdown") {
ipcMessages.forEach((aMessage) => {
cpmm.removeMessageListener(aMessage, this);
});
}
}
};
DownloadsIPC.init();

View File

@ -0,0 +1,15 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
EXTRA_COMPONENTS += [
'DownloadsAPI.js',
'DownloadsAPI.manifest',
]
EXTRA_JS_MODULES += [
'DownloadsAPI.jsm',
'DownloadsIPC.jsm',
]

View File

@ -0,0 +1,9 @@
[DEFAULT]
support-files =
serve_file.sjs
[test_downloads_navigator_object.html]
[test_downloads_basic.html]
[test_downloads_large.html]
[test_downloads_pause_remove.html]
[test_downloads_pause_resume.html]

View File

@ -0,0 +1,7 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
MOCHITEST_MANIFESTS += ['mochitest.ini']

View File

@ -0,0 +1,107 @@
// Serves a file with a given mime type and size at an optionally given rate.
function getQuery(request) {
var query = {};
request.queryString.split('&').forEach(function (val) {
var [name, value] = val.split('=');
query[name] = unescape(value);
});
return query;
}
// Timer used to handle the request response.
var timer = null;
function handleResponse() {
// Is this a rate limited response?
if (this.state.rate > 0) {
// Calculate how many bytes we have left to send.
var bytesToWrite = this.state.totalBytes - this.state.sentBytes;
// Do we have any bytes left to send? If not we'll just fall thru and
// cancel our repeating timer and finalize the response.
if (bytesToWrite > 0) {
// Figure out how many bytes to send, based on the rate limit.
bytesToWrite =
(bytesToWrite > this.state.rate) ? this.state.rate : bytesToWrite;
for (let i = 0; i < bytesToWrite; i++) {
this.response.write("0");
}
// Update the number of bytes we've sent to the client.
this.state.sentBytes += bytesToWrite;
// Wait until the next call to do anything else.
return;
}
}
else {
// Not rate limited, write it all out.
for (let i = 0; i < this.state.totalBytes; i++) {
this.response.write("0");
}
}
// Finalize the response.
this.response.finish();
// All done sending, go ahead and cancel our repeating timer.
timer.cancel();
}
function handleRequest(request, response) {
var query = getQuery(request);
// Default values for content type, size and rate.
var contentType = "text/plain";
var size = 1024;
var rate = 0;
// optional content type to be used by our response.
if ("contentType" in query) {
contentType = query["contentType"];
}
// optional size (in bytes) for generated file.
if ("size" in query) {
size = parseInt(query["size"]);
}
// optional rate (in bytes/s) at which to send the file.
if ("rate" in query) {
rate = parseInt(query["rate"]);
}
// The context for the responseHandler.
var context = {
response: response,
state: {
contentType: contentType,
totalBytes: size,
sentBytes: 0,
rate: rate
}
};
// The notify implementation for the timer.
context.notify = handleResponse.bind(context);
timer =
Components.classes["@mozilla.org/timer;1"]
.createInstance(Components.interfaces.nsITimer);
// sending at a specific rate requires our response to be asynchronous so
// we handle all requests asynchronously. See handleResponse().
response.processAsync();
// generate the content.
response.setHeader("Content-Type", contentType, false);
response.setHeader("Content-Length", size.toString(), false);
// initialize the timer and start writing out the response.
timer.initWithCallback(context,
1000,
Components.interfaces.nsITimer.TYPE_REPEATING_SLACK);
}

View File

@ -0,0 +1,84 @@
<!DOCTYPE html>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=938023
-->
<head>
<title>Test for Bug 938023 Downloads API</title>
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=938023">Mozilla Bug 938023</a>
<p id="display"></p>
<div id="content" style="display: none">
</div>
<a href="serve_file.sjs?contentType=application/octet-stream&size=1024" download="test.bin" id="download1">Download #1</a>
<pre id="test">
<script class="testbody" type="text/javascript;version=1.7">
// Testing a simple download, waiting for it to be done.
SimpleTest.waitForExplicitFinish();
var index = -1;
function next() {
index += 1;
if (index >= steps.length) {
ok(false, "Shouldn't get here!");
return;
}
try {
steps[index]();
} catch(ex) {
ok(false, "Caught exception", ex);
}
}
function downloadChange(evt) {
var download = evt.download;
if (download.state == "succeeded") {
ok(download.totalBytes == 1024, "Download size is 1024 bytes.");
ok(download.contentType == "application/octet-stream",
"contentType is application/octet-stream.");
SimpleTest.finish();
}
}
var steps = [
// Start by setting the pref to true.
function() {
SpecialPowers.pushPrefEnv({
set: [["dom.mozDownloads.enabled", true]]
}, next);
},
// Setup the event listeners.
function() {
SpecialPowers.pushPermissions([
{type: "downloads", allow: true, context: document}
], function() {
navigator.mozDownloadManager.ondownloadstart =
function(evt) {
ok(true, "Download started");
evt.download.addEventListener("statechange", downloadChange);
}
next();
});
},
// Click on the <a download> to start the download.
function() {
document.getElementById("download1").click();
}
];
next();
</script>
</pre>
</body>
</html>

View File

@ -0,0 +1,109 @@
<!DOCTYPE html>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=938023
-->
<head>
<title>Test for Bug 938023 Downloads API</title>
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=938023">Mozilla Bug 938023</a>
<p id="display"></p>
<div id="content" style="display: none">
</div>
<a href="serve_file.sjs?contentType=application/octet-stream&size=102400" download="test.bin" id="download1">Large Download</a>
<pre id="test">
<script class="testbody" type="text/javascript;version=1.7">
// Testing downloading a file, then checking getDownloads() and clearAllDone().
SimpleTest.waitForExplicitFinish();
var index = -1;
function next(args) {
index += 1;
if (index >= steps.length) {
ok(false, "Shouldn't get here!");
return;
}
try {
steps[index](args);
} catch(ex) {
ok(false, "Caught exception", ex);
}
}
// Catch all error function.
function error() {
ok(false, "API failure");
SimpleTest.finish();
}
function getDownloads(downloads) {
ok(downloads.length == 1, "One downloads after getDownloads");
navigator.mozDownloadManager.clearAllDone().then(clearAllDone, error);
}
function clearAllDone(downloads) {
ok(downloads.length == 0, "No downloads after clearAllDone");
SimpleTest.finish();
}
function downloadChange(evt) {
var download = evt.download;
if (download.state == "succeeded") {
ok(download.totalBytes == 102400, "Download size is 100k bytes.");
navigator.mozDownloadManager.getDownloads().then(getDownloads, error);
}
}
var steps = [
// Start by setting the pref to true.
function() {
SpecialPowers.pushPrefEnv({
set: [["dom.mozDownloads.enabled", true]]
}, next);
},
// Setup permission and clear current list.
function() {
SpecialPowers.pushPermissions([
{type: "downloads", allow: true, context: document}
], function() {
navigator.mozDownloadManager.clearAllDone().then(next, error);
});
},
function(downloads) {
ok(downloads.length == 0, "Start with an empty download list.");
next();
},
// Setup the event listeners.
function() {
navigator.mozDownloadManager.ondownloadstart =
function(evt) {
ok(true, "Download started");
evt.download.addEventListener("statechange", downloadChange);
}
next();
},
// Click on the <a download> to start the download.
function() {
document.getElementById("download1").click();
}
];
next();
</script>
</pre>
</body>
</html>

View File

@ -0,0 +1,75 @@
<!DOCTYPE html>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=938023
-->
<head>
<title>Test for Bug 938023 Downloads API</title>
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=938023">Mozilla Bug 938023</a>
<p id="display"></p>
<div id="content" style="display: none">
<iframe></iframe>
</div>
<pre id="test">
<script class="testbody" type="text/javascript;version=1.7">
SimpleTest.waitForExplicitFinish();
var index = -1;
function next() {
index += 1;
if (index >= steps.length) {
ok(false, "Shouldn't get here!");
return;
}
try {
steps[index]();
} catch(ex) {
ok(false, "Caught exception", ex);
}
}
var steps = [
// Start by setting the pref to true.
function() {
SpecialPowers.pushPrefEnv({
set: [["dom.mozDownloads.enabled", true]]
}, next);
},
function() {
SpecialPowers.pushPermissions([
{type: "downloads", allow: 0, context: document}
], function() {
ise(frames[0].navigator.mozDownloadManager, null, "navigator.mozDownloadManager is null when the page doesn't have permissions");
next();
});
},
function() {
SpecialPowers.pushPrefEnv({
set: [["dom.mozDownloads.enabled", false]]
}, function() {
ise(navigator.mozDownloadManager, undefined, "navigator.mozDownloadManager is undefined");
next();
});
},
function() {
SimpleTest.finish();
}
];
next();
</script>
</pre>
</body>
</html>

View File

@ -0,0 +1,116 @@
<!DOCTYPE html>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=938023
-->
<head>
<title>Test for Bug 938023 Downloads API</title>
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=938023">Mozilla Bug 938023</a>
<p id="display"></p>
<div id="content" style="display: none">
</div>
<a href="serve_file.sjs?contentType=application/octet-stream&size=102400&rate=1024" download="test.bin" id="download1">Large Download</a>
<pre id="test">
<script class="testbody" type="text/javascript;version=1.7">
// Testing pausing a download and then removing it.
SimpleTest.waitForExplicitFinish();
var index = -1;
function next(args) {
index += 1;
if (index >= steps.length) {
ok(false, "Shouldn't get here!");
return;
}
try {
steps[index](args);
} catch(ex) {
ok(false, "Caught exception", ex);
}
}
var pausing = false;
// Catch all error function.
function error() {
ok(false, "API failure");
SimpleTest.finish();
}
function checkDownloadList(downloads) {
ok(downloads.length == 0, "No downloads left");
SimpleTest.finish();
}
function checkRemoved(download) {
ok(download.state == "finalized", "Download removed.");
navigator.mozDownloadManager.getDownloads()
.then(checkDownloadList, error);
}
function downloadChange(evt) {
var download = evt.download;
if (download.state == "downloading" && !pausing) {
pausing = true;
download.pause();
} else if (download.state == "stopped") {
ok(pausing, "Download stopped by pause()");
navigator.mozDownloadManager.remove(download)
.then(checkRemoved, error);
}
}
var steps = [
// Start by setting the pref to true.
function() {
SpecialPowers.pushPrefEnv({
set: [["dom.mozDownloads.enabled", true]]
}, next);
},
// Setup permission and clear current list.
function() {
SpecialPowers.pushPermissions([
{type: "downloads", allow: true, context: document}
], function() {
navigator.mozDownloadManager.clearAllDone().then(next, error);
});
},
function(downloads) {
ok(downloads.length == 0, "Start with an empty download list.");
next();
},
// Setup the event listeners.
function() {
navigator.mozDownloadManager.ondownloadstart =
function(evt) {
ok(true, "Download started");
evt.download.addEventListener("statechange", downloadChange);
}
next();
},
// Click on the <a download> to start the download.
function() {
document.getElementById("download1").click();
}
];
next();
</script>
</pre>
</body>
</html>

View File

@ -0,0 +1,119 @@
<!DOCTYPE html>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=938023
-->
<head>
<title>Test for Bug 938023 Downloads API</title>
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=938023">Mozilla Bug 938023</a>
<p id="display"></p>
<div id="content" style="display: none">
</div>
<a href="serve_file.sjs?contentType=application/octet-stream&size=102400&rate=1024" download="test.bin" id="download1">Large Download</a>
<pre id="test">
<script class="testbody" type="text/javascript;version=1.7">
// Testing pausing a download and then resuming it.
SimpleTest.waitForExplicitFinish();
var index = -1;
function next(args) {
index += 1;
if (index >= steps.length) {
ok(false, "Shouldn't get here!");
return;
}
try {
steps[index](args);
} catch(ex) {
ok(false, "Caught exception", ex);
}
}
var pausing = false;
var resuming = false;
// Catch all error function.
function error() {
ok(false, "API failure");
SimpleTest.finish();
}
function checkDownloadList(downloads) {
ok(downloads.length == 0, "No downloads left");
SimpleTest.finish();
}
function checkResumedFailed(download) {
ok(download.state == "stopped", "Download fails to resume.");
navigator.mozDownloadManager.clearAllDone()
.then(checkDownloadList, error);
}
function downloadChange(evt) {
var download = evt.download;
if (download.state == "downloading" && !pausing) {
pausing = true;
download.pause();
} else if (download.state == "stopped" && !resuming) {
resuming = true;
ok(pausing, "Download stopped by pause()");
// serve_file.sjs does not support resuming, so that should fail.
download.resume()
.then(error, function() { checkResumedFailed(download); });
}
}
var steps = [
// Start by setting the pref to true.
function() {
SpecialPowers.pushPrefEnv({
set: [["dom.mozDownloads.enabled", true]]
}, next);
},
// Setup permission and clear current list.
function() {
SpecialPowers.pushPermissions([
{type: "downloads", allow: true, context: document}
], function() {
navigator.mozDownloadManager.clearAllDone().then(next, error);
});
},
function(downloads) {
ok(downloads.length == 0, "Start with an empty download list.");
next();
},
// Setup the event listeners.
function() {
navigator.mozDownloadManager.ondownloadstart =
function(evt) {
ok(true, "Download started");
evt.download.addEventListener("statechange", downloadChange);
}
next();
},
// Click on the <a download> to start the download.
function() {
document.getElementById("download1").click();
}
];
next();
</script>
</pre>
</body>
</html>

View File

@ -104,6 +104,9 @@ if CONFIG['MOZ_GAMEPAD']:
if CONFIG['MOZ_NFC']:
PARALLEL_DIRS += ['nfc']
if CONFIG['MOZ_B2G']:
PARALLEL_DIRS += ['downloads']
TEST_DIRS += [
'tests',
'imptests',

View File

@ -220,6 +220,9 @@ var interfaceNamesInGlobalScope =
"DOMStringMap",
"DOMTokenList",
"DOMTransactionEvent",
{name: "DOMDownload", b2g: true, pref: "dom.mozDownloads.enabled"},
{name: "DOMDownloadManager", b2g: true, pref: "dom.mozDownloads.enabled"},
{name: "DownloadEvent", b2g: true, pref: "dom.mozDownloads.enabled"},
"DragEvent",
"DynamicsCompressorNode",
"Element",

View File

@ -0,0 +1,17 @@
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/.
*/
[Constructor(DOMString type, optional DownloadEventInit eventInitDict),
Pref="dom.mozDownloads.enabled"]
interface DownloadEvent : Event
{
readonly attribute DOMDownload? download;
};
dictionary DownloadEventInit : EventInit
{
DOMDownload? download = null;
};

View File

@ -0,0 +1,75 @@
/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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/.
*/
[NavigatorProperty="mozDownloadManager",
JSImplementation="@mozilla.org/downloads/manager;1",
Pref="dom.mozDownloads.enabled"]
interface DOMDownloadManager : EventTarget {
// This promise returns an array of downloads with all the current
// download objects.
Promise getDownloads();
// Removes one download from the downloads set. Returns a promise resolved
// with the finalized download.
Promise remove(DOMDownload download);
// Removes all the completed downloads from the set.
Promise clearAllDone();
// Fires when a new download starts.
attribute EventHandler ondownloadstart;
};
[JSImplementation="@mozilla.org/downloads/download;1",
Pref="dom.mozDownloads.enabled"]
interface DOMDownload : EventTarget {
// The full size of the resource.
readonly attribute long totalBytes;
// The number of bytes that we have currently downloaded.
readonly attribute long currentBytes;
// The url of the resource.
readonly attribute DOMString url;
// The path in local storage where the file will end up once the download
// is complete.
readonly attribute DOMString path;
// The state of the download. Can be any of:
// "downloading": The resource is actively transfering.
// "stopped" : No network tranfer is happening.
// "succeeded" : The resource has been downloaded successfully.
// "finalized" : We won't try to download this resource, but the DOM
// object is still alive.
readonly attribute DOMString state;
// The mime type for this resource.
readonly attribute DOMString contentType;
// The timestamp this download started.
readonly attribute Date startTime;
// An opaque identifier for this download. All instances of the same
// download (eg. in different windows) will have the same id.
readonly attribute DOMString id;
// A DOM error object, that will be not null when a download is stopped
// because something failed.
readonly attribute DOMError error;
// Pauses the download.
Promise pause();
// Resumes the download. This resolves only once the download has
// succeeded.
Promise resume();
// This event is triggered anytime a property of the object changes:
// - when the transfer progresses, updating currentBytes.
// - when the state and/or error attributes change.
attribute EventHandler onstatechange;
};

View File

@ -86,6 +86,7 @@ WEBIDL_FILES = [
'DOMStringMap.webidl',
'DOMTokenList.webidl',
'DOMTransaction.webidl',
'Downloads.webidl',
'DragEvent.webidl',
'DummyBinding.webidl',
'DynamicsCompressorNode.webidl',
@ -559,6 +560,7 @@ GENERATED_EVENTS_WEBIDL_FILES = [
'DataStoreChangeEvent.webidl',
'DeviceLightEvent.webidl',
'DeviceProximityEvent.webidl',
'DownloadEvent.webidl',
'ErrorEvent.webidl',
'IccChangeEvent.webidl',
'MediaStreamEvent.webidl',

View File

@ -308,7 +308,16 @@ this.DownloadIntegration = {
// progress, as well as stopped downloads for which we retained partially
// downloaded data. Stopped downloads for which we don't need to track the
// presence of a ".part" file are only retained in the browser history.
// On b2g, we keep a few days of history.
#ifdef MOZ_B2G
let maxTime = Date.now() -
Services.prefs.getIntPref("dom.downloads.max_retention_days") * 24 * 60 * 60 * 1000;
return (aDownload.startTime > maxTime) ||
aDownload.hasPartialData ||
!aDownload.stopped;
#else
return aDownload.hasPartialData || !aDownload.stopped;
#endif
},
/**

View File

@ -108,7 +108,7 @@ XPCOMUtils.defineLazyGetter(DownloadUIHelper, "strings", function () {
*/
this.DownloadPrompter = function (aParent)
{
#ifdef MOZ_WIDGET_GONK
#ifdef MOZ_B2G
// On B2G there is no prompter implementation.
this._prompter = null;
#else