mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-28 23:31:56 +00:00
Revert the backout of bug 938023 (changesets 19fbd3fb0373:8146150d4df8)
This commit is contained in:
parent
ddbcbb1b36
commit
1221083b0e
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -60,3 +60,5 @@ if test "$OS_TARGET" = "Android"; then
|
||||
MOZ_NUWA_PROCESS=
|
||||
fi
|
||||
MOZ_FOLD_LIBS=1
|
||||
|
||||
MOZ_JSDOWNLOADS=1
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
},
|
||||
|
@ -318,6 +318,11 @@ this.PermissionsTable = { geolocation: {
|
||||
privileged: ALLOW_ACTION,
|
||||
certified: ALLOW_ACTION
|
||||
},
|
||||
"downloads": {
|
||||
app: DENY_ACTION,
|
||||
privileged: DENY_ACTION,
|
||||
certified: ALLOW_ACTION
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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) {
|
||||
|
@ -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
10
dom/downloads/moz.build
Normal 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']
|
320
dom/downloads/src/DownloadsAPI.js
Normal file
320
dom/downloads/src/DownloadsAPI.js
Normal 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]);
|
255
dom/downloads/src/DownloadsAPI.jsm
Normal file
255
dom/downloads/src/DownloadsAPI.jsm
Normal 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();
|
6
dom/downloads/src/DownloadsAPI.manifest
Normal file
6
dom/downloads/src/DownloadsAPI.manifest
Normal 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}
|
221
dom/downloads/src/DownloadsIPC.jsm
Normal file
221
dom/downloads/src/DownloadsIPC.jsm
Normal 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();
|
15
dom/downloads/src/moz.build
Normal file
15
dom/downloads/src/moz.build
Normal 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',
|
||||
]
|
9
dom/downloads/tests/mochitest.ini
Normal file
9
dom/downloads/tests/mochitest.ini
Normal 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]
|
7
dom/downloads/tests/moz.build
Normal file
7
dom/downloads/tests/moz.build
Normal 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']
|
107
dom/downloads/tests/serve_file.sjs
Normal file
107
dom/downloads/tests/serve_file.sjs
Normal 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);
|
||||
|
||||
}
|
84
dom/downloads/tests/test_downloads_basic.html
Normal file
84
dom/downloads/tests/test_downloads_basic.html
Normal 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>
|
109
dom/downloads/tests/test_downloads_large.html
Normal file
109
dom/downloads/tests/test_downloads_large.html
Normal 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>
|
75
dom/downloads/tests/test_downloads_navigator_object.html
Normal file
75
dom/downloads/tests/test_downloads_navigator_object.html
Normal 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>
|
116
dom/downloads/tests/test_downloads_pause_remove.html
Normal file
116
dom/downloads/tests/test_downloads_pause_remove.html
Normal 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>
|
119
dom/downloads/tests/test_downloads_pause_resume.html
Normal file
119
dom/downloads/tests/test_downloads_pause_resume.html
Normal 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>
|
@ -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',
|
||||
|
@ -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",
|
||||
|
17
dom/webidl/DownloadEvent.webidl
Normal file
17
dom/webidl/DownloadEvent.webidl
Normal 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;
|
||||
};
|
75
dom/webidl/Downloads.webidl
Normal file
75
dom/webidl/Downloads.webidl
Normal 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;
|
||||
};
|
@ -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',
|
||||
|
@ -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
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user