Merge m-c to inbound.

This commit is contained in:
Ryan VanderMeulen 2013-08-21 17:17:18 -04:00
commit 63ae93c3cb
62 changed files with 2272 additions and 1084 deletions

View File

@ -212,6 +212,7 @@ let FormAssistant = {
scrollIntoViewTimeout: null,
_focusedElement: null,
_focusCounter: 0, // up one for every time we focus a new element
_observer: null,
_documentEncoder: null,
_editor: null,
_editing: false,
@ -230,12 +231,18 @@ let FormAssistant = {
},
setFocusedElement: function fa_setFocusedElement(element) {
let self = this;
if (element === this.focusedElement)
return;
if (this.focusedElement) {
this.focusedElement.removeEventListener('mousedown', this);
this.focusedElement.removeEventListener('mouseup', this);
if (this._observer) {
this._observer.disconnect();
this._observer = null;
}
if (!element) {
this.focusedElement.blur();
}
@ -265,6 +272,24 @@ let FormAssistant = {
// element.
this._editor.addEditorObserver(this);
}
// If our focusedElement is removed from DOM we want to handle it properly
let MutationObserver = element.ownerDocument.defaultView.MutationObserver;
this._observer = new MutationObserver(function(mutations) {
var del = [].some.call(mutations, function(m) {
return [].some.call(m.removedNodes, function(n) {
return n === element;
});
});
if (del && element === this.focusedElement) {
// item was deleted, fake a blur so all state gets set correctly
self.handleEvent({ target: element, type: "blur" });
}
});
this._observer.observe(element.parentNode, {
childList: true
});
}
this.focusedElement = element;

View File

@ -1,4 +1,4 @@
{
"revision": "085648d44f2a1ad2c73b0eb4225c6448104e70f5",
"revision": "e4f586d6a2c7a8ff3a676d2ddfecc5b2de94f429",
"repo_path": "/integration/gaia-central"
}

View File

@ -336,7 +336,7 @@ pref("browser.download.manager.scanWhenDone", true);
pref("browser.download.manager.resumeOnWakeDelay", 10000);
// Enables the asynchronous Downloads API in the Downloads Panel.
pref("browser.download.useJSTransfer", false);
pref("browser.download.useJSTransfer", true);
// This allows disabling the Downloads Panel in favor of the old interface.
pref("browser.download.useToolkitUI", false);

View File

@ -112,10 +112,7 @@
}
function reloadProvider() {
Social.enabled = false;
Services.tm.mainThread.dispatch(function() {
Social.enabled = true;
}, Components.interfaces.nsIThread.DISPATCH_NORMAL);
Social.provider.reload();
}
parseQueryString();

View File

@ -36,6 +36,7 @@ SocialUI = {
Services.obs.addObserver(this, "social:frameworker-error", false);
Services.obs.addObserver(this, "social:provider-set", false);
Services.obs.addObserver(this, "social:providers-changed", false);
Services.obs.addObserver(this, "social:provider-reload", false);
Services.prefs.addObserver("social.sidebar.open", this, false);
Services.prefs.addObserver("social.toast-notifications.enabled", this, false);
@ -60,6 +61,7 @@ SocialUI = {
Services.obs.removeObserver(this, "social:frameworker-error");
Services.obs.removeObserver(this, "social:provider-set");
Services.obs.removeObserver(this, "social:providers-changed");
Services.obs.removeObserver(this, "social:provider-reload");
Services.prefs.removeObserver("social.sidebar.open", this);
Services.prefs.removeObserver("social.toast-notifications.enabled", this);
@ -74,6 +76,16 @@ SocialUI = {
// manually :(
try {
switch (topic) {
case "social:provider-reload":
// if the reloaded provider is our current provider, fall through
// to social:provider-set so the ui will be reset
if (!Social.provider || Social.provider.origin != data)
return;
// be sure to unload the sidebar as it will not reload if the origin
// has not changed, it will be loaded in provider-set below. Other
// panels will be unloaded or handle reload.
SocialSidebar.unloadSidebar();
// fall through to social:provider-set
case "social:provider-set":
// Social.provider has changed (possibly to null), update any state
// which depends on it.
@ -142,7 +154,7 @@ SocialUI = {
// Miscellaneous helpers
showProfile: function SocialUI_showProfile() {
if (Social.haveLoggedInUser())
if (Social.provider.haveLoggedInUser())
openUILinkIn(Social.provider.profile.profileURL, "tab");
else {
// XXX Bug 789585 will implement an API for provider-specified login pages.
@ -976,22 +988,20 @@ SocialToolbar = {
let toggleNotificationsCommand = document.getElementById("Social:ToggleNotifications");
toggleNotificationsCommand.setAttribute("hidden", !socialEnabled);
if (!Social.haveLoggedInUser() || !socialEnabled) {
let parent = document.getElementById("social-notification-panel");
while (parent.hasChildNodes()) {
let frame = parent.firstChild;
SharedFrame.forgetGroup(frame.id);
parent.removeChild(frame);
}
let parent = document.getElementById("social-notification-panel");
while (parent.hasChildNodes()) {
let frame = parent.firstChild;
SharedFrame.forgetGroup(frame.id);
parent.removeChild(frame);
}
let tbi = document.getElementById("social-toolbar-item");
if (tbi) {
// SocialMark is the last button allways
let next = SocialMark.button.previousSibling;
while (next != this.button) {
tbi.removeChild(next);
next = SocialMark.button.previousSibling;
}
let tbi = document.getElementById("social-toolbar-item");
if (tbi) {
// SocialMark is the last button allways
let next = SocialMark.button.previousSibling;
while (next != this.button) {
tbi.removeChild(next);
next = SocialMark.button.previousSibling;
}
}
},
@ -1035,7 +1045,7 @@ SocialToolbar = {
// provider.profile == undefined means no response yet from the provider
// to tell us whether the user is logged in or not.
if (!SocialUI.enabled ||
(!Social.haveLoggedInUser() && Social.provider.profile !== undefined)) {
(!Social.provider.haveLoggedInUser() && Social.provider.profile !== undefined)) {
// Either no enabled provider, or there is a provider and it has
// responded with a profile and the user isn't loggedin. The icons
// etc have already been removed by updateButtonHiddenState, so we want

View File

@ -905,10 +905,7 @@ nsContextMenu.prototype = {
reload: function(event) {
if (this.onSocial) {
// full reload of social provider
Social.enabled = false;
Services.tm.mainThread.dispatch(function() {
Social.enabled = true;
}, Components.interfaces.nsIThread.DISPATCH_NORMAL);
Social.provider.reload();
} else {
BrowserReloadOrDuplicate(event);
}

View File

@ -16,7 +16,7 @@ XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon",
"resource:///modules/DownloadsCommon.jsm");
function Sanitizer() {}
Sanitizer.prototype = {
// warning to the caller: this one may raise an exception (e.g. bug #265028)
@ -37,14 +37,14 @@ Sanitizer.prototype = {
aCallback(aItemName, canClear, aArg);
return canClear;
},
prefDomain: "",
getNameFromPreference: function (aPreferenceName)
{
return aPreferenceName.substr(this.prefDomain.length);
},
/**
* Deletes privacy sensitive data in a batch, according to user preferences.
* Returns a promise which is resolved if no errors occurred. If an error
@ -87,7 +87,8 @@ Sanitizer.prototype = {
item.clear();
} catch(er) {
seenError = true;
Cu.reportError("Error sanitizing " + itemName + ": " + er + "\n");
Components.utils.reportError("Error sanitizing " + itemName +
": " + er + "\n");
}
onItemComplete();
};
@ -99,7 +100,7 @@ Sanitizer.prototype = {
return deferred.promise;
},
// Time span only makes sense in certain cases. Consumers who want
// to only clear some private data can opt in by setting this to false,
// and can optionally specify a specific range. If timespan is not ignored,
@ -107,7 +108,7 @@ Sanitizer.prototype = {
// pref to determine a range
ignoreTimespan : true,
range : null,
items: {
cache: {
clear: function ()
@ -126,13 +127,13 @@ Sanitizer.prototype = {
imageCache.clearCache(false); // true=chrome, false=content
} catch(er) {}
},
get canClear()
{
return true;
}
},
cookies: {
clear: function ()
{
@ -143,7 +144,7 @@ Sanitizer.prototype = {
var cookiesEnum = cookieMgr.enumerator;
while (cookiesEnum.hasMoreElements()) {
var cookie = cookiesEnum.getNext().QueryInterface(Ci.nsICookie2);
if (cookie.creationTime > this.range[0])
// This cookie was created after our cutoff, clear it
cookieMgr.remove(cookie.host, cookie.name, cookie.path, false);
@ -211,14 +212,14 @@ Sanitizer.prototype = {
PlacesUtils.history.removeVisitsByTimeframe(this.range[0], this.range[1]);
else
PlacesUtils.history.removeAllPages();
try {
var os = Components.classes["@mozilla.org/observer-service;1"]
.getService(Components.interfaces.nsIObserverService);
os.notifyObservers(null, "browser:purge-session-history", "");
}
catch (e) { }
// Clear last URL of the Open Web Location dialog
var prefs = Components.classes["@mozilla.org/preferences-service;1"]
.getService(Components.interfaces.nsIPrefBranch);
@ -227,7 +228,7 @@ Sanitizer.prototype = {
}
catch (e) { }
},
get canClear()
{
// bug 347231: Always allow clearing history due to dependencies on
@ -235,7 +236,7 @@ Sanitizer.prototype = {
return true;
}
},
formdata: {
clear: function ()
{
@ -305,15 +306,20 @@ Sanitizer.prototype = {
return false;
}
},
downloads: {
clear: function ()
{
if (DownloadsCommon.useJSTransfer) {
Task.spawn(function () {
let filterByTime = this.range ?
(download => download.startTime >= this.range[0] &&
download.startTime <= this.range[1]) : null;
let filterByTime = null;
if (this.range) {
// Convert microseconds back to milliseconds for date comparisons.
let rangeBeginMs = this.range[0] / 1000;
let rangeEndMs = this.range[1] / 1000;
filterByTime = download => download.startTime >= rangeBeginMs &&
download.startTime <= rangeEndMs;
}
// Clear all completed/cancelled downloads
let publicList = yield Downloads.getPublicDownloadList();
@ -321,7 +327,7 @@ Sanitizer.prototype = {
let privateList = yield Downloads.getPrivateDownloadList();
privateList.removeFinished(filterByTime);
}.bind(this)).then(null, Cu.reportError);
}.bind(this)).then(null, Components.utils.reportError);
}
else {
var dlMgr = Components.classes["@mozilla.org/download-manager;1"]
@ -352,7 +358,7 @@ Sanitizer.prototype = {
return false;
}
},
passwords: {
clear: function ()
{
@ -361,7 +367,7 @@ Sanitizer.prototype = {
// Passwords are timeless, and don't respect the timeSpan setting
pwmgr.removeAllLogins();
},
get canClear()
{
var pwmgr = Components.classes["@mozilla.org/login-manager;1"]
@ -370,7 +376,7 @@ Sanitizer.prototype = {
return (count > 0);
}
},
sessions: {
clear: function ()
{
@ -384,13 +390,13 @@ Sanitizer.prototype = {
.getService(Components.interfaces.nsIObserverService);
os.notifyObservers(null, "net:clear-active-logins", null);
},
get canClear()
{
return true;
}
},
siteSettings: {
clear: function ()
{
@ -398,12 +404,12 @@ Sanitizer.prototype = {
var pm = Components.classes["@mozilla.org/permissionmanager;1"]
.getService(Components.interfaces.nsIPermissionManager);
pm.removeAll();
// Clear site-specific settings like page-zoom level
var cps = Components.classes["@mozilla.org/content-pref/service;1"]
.getService(Components.interfaces.nsIContentPrefService2);
cps.removeAllDomains(null);
// Clear "Never remember passwords for this site", which is not handled by
// the permission manager
var pwmgr = Components.classes["@mozilla.org/login-manager;1"]
@ -413,7 +419,7 @@ Sanitizer.prototype = {
pwmgr.setLoginSavingEnabled(host, true);
}
},
get canClear()
{
return true;
@ -446,7 +452,7 @@ Sanitizer.getClearRange = function (ts) {
ts = Sanitizer.prefs.getIntPref("timeSpan");
if (ts === Sanitizer.TIMESPAN_EVERYTHING)
return null;
// PRTime is microseconds while JS time is milliseconds
var endDate = Date.now() * 1000;
switch (ts) {
@ -473,7 +479,7 @@ Sanitizer.getClearRange = function (ts) {
};
Sanitizer._prefs = null;
Sanitizer.__defineGetter__("prefs", function()
Sanitizer.__defineGetter__("prefs", function()
{
return Sanitizer._prefs ? Sanitizer._prefs
: Sanitizer._prefs = Components.classes["@mozilla.org/preferences-service;1"]
@ -482,7 +488,7 @@ Sanitizer.__defineGetter__("prefs", function()
});
// Shows sanitization UI
Sanitizer.showUI = function(aParentWindow)
Sanitizer.showUI = function(aParentWindow)
{
var ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
.getService(Components.interfaces.nsIWindowWatcher);
@ -497,32 +503,32 @@ Sanitizer.showUI = function(aParentWindow)
null);
};
/**
* Deletes privacy sensitive data in a batch, optionally showing the
/**
* Deletes privacy sensitive data in a batch, optionally showing the
* sanitize UI, according to user preferences
*/
Sanitizer.sanitize = function(aParentWindow)
Sanitizer.sanitize = function(aParentWindow)
{
Sanitizer.showUI(aParentWindow);
};
Sanitizer.onStartup = function()
Sanitizer.onStartup = function()
{
// we check for unclean exit with pending sanitization
Sanitizer._checkAndSanitize();
};
Sanitizer.onShutdown = function()
Sanitizer.onShutdown = function()
{
// we check if sanitization is needed and perform it
Sanitizer._checkAndSanitize();
};
// this is called on startup and shutdown, to perform pending sanitizations
Sanitizer._checkAndSanitize = function()
Sanitizer._checkAndSanitize = function()
{
const prefs = Sanitizer.prefs;
if (prefs.getBoolPref(Sanitizer.prefShutdown) &&
if (prefs.getBoolPref(Sanitizer.prefShutdown) &&
!prefs.prefHasUserValue(Sanitizer.prefDidShutdown)) {
// this is a shutdown or a startup after an unclean exit
var s = new Sanitizer();

View File

@ -2,10 +2,10 @@ Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
// Bug 453440 - Test the timespan-based logic of the sanitizer code
var now_uSec = Date.now() * 1000;
const dm = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
let now_mSec = Date.now();
let now_uSec = now_mSec * 1000;
const kMsecPerMin = 60 * 1000;
const kUsecPerMin = 60 * 1000000;
let tempScope = {};
@ -14,6 +14,7 @@ Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader)
let Sanitizer = tempScope.Sanitizer;
let FormHistory = (Components.utils.import("resource://gre/modules/FormHistory.jsm", {})).FormHistory;
let Downloads = (Components.utils.import("resource://gre/modules/Downloads.jsm", {})).Downloads;
function promiseFormHistoryRemoved() {
let deferred = Promise.defer();
@ -24,15 +25,30 @@ function promiseFormHistoryRemoved() {
return deferred.promise;
}
function promiseDownloadRemoved(list) {
let deferred = Promise.defer();
let view = {
onDownloadRemoved: function(download) {
list.removeView(view);
deferred.resolve();
}
};
list.addView(view);
return deferred.promise;
}
function test() {
waitForExplicitFinish();
Task.spawn(function() {
setupDownloads();
yield setupDownloads();
yield setupFormHistory();
yield setupHistory();
yield onHistoryReady();
}).then(finish);
}).then(null, ex => ok(false, ex)).then(finish);
}
function countEntries(name, message, check) {
@ -80,12 +96,16 @@ function onHistoryReady() {
itemPrefs.setBoolPref("sessions", false);
itemPrefs.setBoolPref("siteSettings", false);
let publicList = yield Downloads.getPublicDownloadList();
let downloadPromise = promiseDownloadRemoved(publicList);
// Clear 10 minutes ago
s.range = [now_uSec - 10*60*1000000, now_uSec];
s.sanitize();
s.range = null;
yield promiseFormHistoryRemoved();
yield downloadPromise;
ok(!(yield promiseIsURIVisited(makeURI("http://10minutes.com"))),
"Pretend visit to 10minutes.com should now be deleted");
@ -122,23 +142,26 @@ function onHistoryReady() {
yield countEntries("today", "today form entry should still exist", checkOne);
yield countEntries("b4today", "b4today form entry should still exist", checkOne);
ok(!downloadExists(5555555), "10 minute download should now be deleted");
ok(downloadExists(5555551), "<1 hour download should still be present");
ok(downloadExists(5555556), "1 hour 10 minute download should still be present");
ok(downloadExists(5555550), "Year old download should still be present");
ok(downloadExists(5555552), "<2 hour old download should still be present");
ok(downloadExists(5555557), "2 hour 10 minute download should still be present");
ok(downloadExists(5555553), "<4 hour old download should still be present");
ok(downloadExists(5555558), "4 hour 10 minute download should still be present");
ok(!(yield downloadExists(publicList, "fakefile-10-minutes")), "10 minute download should now be deleted");
ok((yield downloadExists(publicList, "fakefile-1-hour")), "<1 hour download should still be present");
ok((yield downloadExists(publicList, "fakefile-1-hour-10-minutes")), "1 hour 10 minute download should still be present");
ok((yield downloadExists(publicList, "fakefile-old")), "Year old download should still be present");
ok((yield downloadExists(publicList, "fakefile-2-hour")), "<2 hour old download should still be present");
ok((yield downloadExists(publicList, "fakefile-2-hour-10-minutes")), "2 hour 10 minute download should still be present");
ok((yield downloadExists(publicList, "fakefile-4-hour")), "<4 hour old download should still be present");
ok((yield downloadExists(publicList, "fakefile-4-hour-10-minutes")), "4 hour 10 minute download should still be present");
if (minutesSinceMidnight > 10)
ok(downloadExists(5555554), "'Today' download should still be present");
ok((yield downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
downloadPromise = promiseDownloadRemoved(publicList);
// Clear 1 hour
Sanitizer.prefs.setIntPref("timeSpan", 1);
s.sanitize();
yield promiseFormHistoryRemoved();
yield downloadPromise;
ok(!(yield promiseIsURIVisited(makeURI("http://1hour.com"))),
"Pretend visit to 1hour.com should now be deleted");
@ -169,23 +192,26 @@ function onHistoryReady() {
yield countEntries("today", "today form entry should still exist", checkOne);
yield countEntries("b4today", "b4today form entry should still exist", checkOne);
ok(!downloadExists(5555551), "<1 hour download should now be deleted");
ok(downloadExists(5555556), "1 hour 10 minute download should still be present");
ok(downloadExists(5555550), "Year old download should still be present");
ok(downloadExists(5555552), "<2 hour old download should still be present");
ok(downloadExists(5555557), "2 hour 10 minute download should still be present");
ok(downloadExists(5555553), "<4 hour old download should still be present");
ok(downloadExists(5555558), "4 hour 10 minute download should still be present");
ok(!(yield downloadExists(publicList, "fakefile-1-hour")), "<1 hour download should now be deleted");
ok((yield downloadExists(publicList, "fakefile-1-hour-10-minutes")), "1 hour 10 minute download should still be present");
ok((yield downloadExists(publicList, "fakefile-old")), "Year old download should still be present");
ok((yield downloadExists(publicList, "fakefile-2-hour")), "<2 hour old download should still be present");
ok((yield downloadExists(publicList, "fakefile-2-hour-10-minutes")), "2 hour 10 minute download should still be present");
ok((yield downloadExists(publicList, "fakefile-4-hour")), "<4 hour old download should still be present");
ok((yield downloadExists(publicList, "fakefile-4-hour-10-minutes")), "4 hour 10 minute download should still be present");
if (hoursSinceMidnight > 1)
ok(downloadExists(5555554), "'Today' download should still be present");
ok((yield downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
downloadPromise = promiseDownloadRemoved(publicList);
// Clear 1 hour 10 minutes
s.range = [now_uSec - 70*60*1000000, now_uSec];
s.sanitize();
s.range = null;
yield promiseFormHistoryRemoved();
yield downloadPromise;
ok(!(yield promiseIsURIVisited(makeURI("http://1hour10minutes.com"))),
"Pretend visit to 1hour10minutes.com should now be deleted");
@ -213,20 +239,23 @@ function onHistoryReady() {
yield countEntries("today", "today form entry should still exist", checkOne);
yield countEntries("b4today", "b4today form entry should still exist", checkOne);
ok(!downloadExists(5555556), "1 hour 10 minute old download should now be deleted");
ok(downloadExists(5555550), "Year old download should still be present");
ok(downloadExists(5555552), "<2 hour old download should still be present");
ok(downloadExists(5555557), "2 hour 10 minute download should still be present");
ok(downloadExists(5555553), "<4 hour old download should still be present");
ok(downloadExists(5555558), "4 hour 10 minute download should still be present");
ok(!(yield downloadExists(publicList, "fakefile-1-hour-10-minutes")), "1 hour 10 minute old download should now be deleted");
ok((yield downloadExists(publicList, "fakefile-old")), "Year old download should still be present");
ok((yield downloadExists(publicList, "fakefile-2-hour")), "<2 hour old download should still be present");
ok((yield downloadExists(publicList, "fakefile-2-hour-10-minutes")), "2 hour 10 minute download should still be present");
ok((yield downloadExists(publicList, "fakefile-4-hour")), "<4 hour old download should still be present");
ok((yield downloadExists(publicList, "fakefile-4-hour-10-minutes")), "4 hour 10 minute download should still be present");
if (minutesSinceMidnight > 70)
ok(downloadExists(5555554), "'Today' download should still be present");
ok((yield downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
downloadPromise = promiseDownloadRemoved(publicList);
// Clear 2 hours
Sanitizer.prefs.setIntPref("timeSpan", 2);
s.sanitize();
yield promiseFormHistoryRemoved();
yield downloadPromise;
ok(!(yield promiseIsURIVisited(makeURI("http://2hour.com"))),
"Pretend visit to 2hour.com should now be deleted");
@ -251,20 +280,23 @@ function onHistoryReady() {
yield countEntries("today", "today form entry should still exist", checkOne);
yield countEntries("b4today", "b4today form entry should still exist", checkOne);
ok(!downloadExists(5555552), "<2 hour old download should now be deleted");
ok(downloadExists(5555550), "Year old download should still be present");
ok(downloadExists(5555557), "2 hour 10 minute download should still be present");
ok(downloadExists(5555553), "<4 hour old download should still be present");
ok(downloadExists(5555558), "4 hour 10 minute download should still be present");
ok(!(yield downloadExists(publicList, "fakefile-2-hour")), "<2 hour old download should now be deleted");
ok((yield downloadExists(publicList, "fakefile-old")), "Year old download should still be present");
ok((yield downloadExists(publicList, "fakefile-2-hour-10-minutes")), "2 hour 10 minute download should still be present");
ok((yield downloadExists(publicList, "fakefile-4-hour")), "<4 hour old download should still be present");
ok((yield downloadExists(publicList, "fakefile-4-hour-10-minutes")), "4 hour 10 minute download should still be present");
if (hoursSinceMidnight > 2)
ok(downloadExists(5555554), "'Today' download should still be present");
ok((yield downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
downloadPromise = promiseDownloadRemoved(publicList);
// Clear 2 hours 10 minutes
s.range = [now_uSec - 130*60*1000000, now_uSec];
s.sanitize();
s.range = null;
yield promiseFormHistoryRemoved();
yield downloadPromise;
ok(!(yield promiseIsURIVisited(makeURI("http://2hour10minutes.com"))),
"Pretend visit to 2hour10minutes.com should now be deleted");
@ -286,18 +318,21 @@ function onHistoryReady() {
yield countEntries("today", "today form entry should still exist", checkOne);
yield countEntries("b4today", "b4today form entry should still exist", checkOne);
ok(!downloadExists(5555557), "2 hour 10 minute old download should now be deleted");
ok(downloadExists(5555553), "<4 hour old download should still be present");
ok(downloadExists(5555558), "4 hour 10 minute download should still be present");
ok(downloadExists(5555550), "Year old download should still be present");
ok(!(yield downloadExists(publicList, "fakefile-2-hour-10-minutes")), "2 hour 10 minute old download should now be deleted");
ok((yield downloadExists(publicList, "fakefile-4-hour")), "<4 hour old download should still be present");
ok((yield downloadExists(publicList, "fakefile-4-hour-10-minutes")), "4 hour 10 minute download should still be present");
ok((yield downloadExists(publicList, "fakefile-old")), "Year old download should still be present");
if (minutesSinceMidnight > 130)
ok(downloadExists(5555554), "'Today' download should still be present");
ok((yield downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
downloadPromise = promiseDownloadRemoved(publicList);
// Clear 4 hours
Sanitizer.prefs.setIntPref("timeSpan", 3);
s.sanitize();
yield promiseFormHistoryRemoved();
yield downloadPromise;
ok(!(yield promiseIsURIVisited(makeURI("http://4hour.com"))),
"Pretend visit to 4hour.com should now be deleted");
@ -316,11 +351,13 @@ function onHistoryReady() {
yield countEntries("today", "today form entry should still exist", checkOne);
yield countEntries("b4today", "b4today form entry should still exist", checkOne);
ok(!downloadExists(5555553), "<4 hour old download should now be deleted");
ok(downloadExists(5555558), "4 hour 10 minute download should still be present");
ok(downloadExists(5555550), "Year old download should still be present");
ok(!(yield downloadExists(publicList, "fakefile-4-hour")), "<4 hour old download should now be deleted");
ok((yield downloadExists(publicList, "fakefile-4-hour-10-minutes")), "4 hour 10 minute download should still be present");
ok((yield downloadExists(publicList, "fakefile-old")), "Year old download should still be present");
if (hoursSinceMidnight > 4)
ok(downloadExists(5555554), "'Today' download should still be present");
ok((yield downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
downloadPromise = promiseDownloadRemoved(publicList);
// Clear 4 hours 10 minutes
s.range = [now_uSec - 250*60*1000000, now_uSec];
@ -328,6 +365,7 @@ function onHistoryReady() {
s.range = null;
yield promiseFormHistoryRemoved();
yield downloadPromise;
ok(!(yield promiseIsURIVisited(makeURI("http://4hour10minutes.com"))),
"Pretend visit to 4hour10minutes.com should now be deleted");
@ -343,48 +381,60 @@ function onHistoryReady() {
yield countEntries("today", "today form entry should still exist", checkOne);
yield countEntries("b4today", "b4today form entry should still exist", checkOne);
ok(!downloadExists(5555558), "4 hour 10 minute download should now be deleted");
ok(downloadExists(5555550), "Year old download should still be present");
ok(!(yield downloadExists(publicList, "fakefile-4-hour-10-minutes")), "4 hour 10 minute download should now be deleted");
ok((yield downloadExists(publicList, "fakefile-old")), "Year old download should still be present");
if (minutesSinceMidnight > 250)
ok(downloadExists(5555554), "'Today' download should still be present");
ok((yield downloadExists(publicList, "fakefile-today")), "'Today' download should still be present");
// The 'Today' download might have been already deleted, in which case we
// should not wait for a download removal notification.
if (minutesSinceMidnight > 250) {
downloadPromise = promiseDownloadRemoved(publicList);
} else {
downloadPromise = Promise.resolve();
}
// Clear Today
Sanitizer.prefs.setIntPref("timeSpan", 4);
s.sanitize();
yield promiseFormHistoryRemoved();
yield downloadPromise;
// Be careful. If we add our objectss just before midnight, and sanitize
// runs immediately after, they won't be expired. This is expected, but
// we should not test in that case. We cannot just test for opposite
// condition because we could cross midnight just one moment after we
// cache our time, then we would have an even worse random failure.
var today = isToday(new Date(now_uSec/1000));
var today = isToday(new Date(now_mSec));
if (today) {
ok(!(yield promiseIsURIVisited(makeURI("http://today.com"))),
"Pretend visit to today.com should now be deleted");
yield countEntries("today", "today form entry should be deleted", checkZero);
ok(!downloadExists(5555554), "'Today' download should now be deleted");
ok(!(yield downloadExists(publicList, "fakefile-today")), "'Today' download should now be deleted");
}
ok((yield promiseIsURIVisited(makeURI("http://before-today.com"))),
"Pretend visit to before-today.com should still exist");
yield countEntries("b4today", "b4today form entry should still exist", checkOne);
ok(downloadExists(5555550), "Year old download should still be present");
ok((yield downloadExists(publicList, "fakefile-old")), "Year old download should still be present");
downloadPromise = promiseDownloadRemoved(publicList);
// Choose everything
Sanitizer.prefs.setIntPref("timeSpan", 0);
s.sanitize();
yield promiseFormHistoryRemoved();
yield downloadPromise;
ok(!(yield promiseIsURIVisited(makeURI("http://before-today.com"))),
"Pretend visit to before-today.com should now be deleted");
yield countEntries("b4today", "b4today form entry should be deleted", checkZero);
ok(!downloadExists(5555550), "Year old download should now be deleted");
ok(!(yield downloadExists(publicList, "fakefile-old")), "Year old download should now be deleted");
}
function setupHistory() {
@ -562,227 +612,103 @@ function setupFormHistory() {
function setupDownloads() {
// Add 10-minutes download to DB
let data = {
id: "5555555",
name: "fakefile-10-minutes",
source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169",
target: "fakefile-10-minutes",
startTime: now_uSec - 10 * kUsecPerMin, // 10 minutes ago, in uSec
endTime: now_uSec - 11 * kUsecPerMin, // 1 minute later
state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED,
currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0,
guid: "a1bcD23eF4g5"
};
let publicList = yield Downloads.getPublicDownloadList();
let db = dm.DBConnection;
let stmt = db.createStatement(
"INSERT INTO moz_downloads (id, name, source, target, startTime, endTime, " +
"state, currBytes, maxBytes, preferredAction, autoResume, guid) " +
"VALUES (:id, :name, :source, :target, :startTime, :endTime, :state, " +
":currBytes, :maxBytes, :preferredAction, :autoResume, :guid)");
try {
for (let prop in data)
stmt.params[prop] = data[prop];
stmt.execute();
}
finally {
stmt.reset();
}
// Add within-1-hour download to DB
data = {
id: "5555551",
name: "fakefile-1-hour",
let download = yield Downloads.createDownload({
source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169",
target: "fakefile-10-minutes"
});
download.startTime = new Date(now_mSec - 10 * kMsecPerMin), // 10 minutes ago
download.canceled = true;
publicList.add(download);
download = yield Downloads.createDownload({
source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440",
target: "fakefile-1-hour",
startTime: now_uSec - 45 * kUsecPerMin, // 45 minutes ago, in uSec
endTime: now_uSec - 44 * kUsecPerMin, // 1 minute later
state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED,
currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0,
guid: "1bcD23eF4g5a"
};
target: "fakefile-1-hour"
});
download.startTime = new Date(now_mSec - 45 * kMsecPerMin), // 45 minutes ago
download.canceled = true;
publicList.add(download);
try {
for (let prop in data)
stmt.params[prop] = data[prop];
stmt.execute();
}
finally {
stmt.reset();
}
// Add 1-hour-10-minutes download to DB
data = {
id: "5555556",
name: "fakefile-1-hour-10-minutes",
download = yield Downloads.createDownload({
source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169",
target: "fakefile-1-hour-10-minutes",
startTime: now_uSec - 70 * kUsecPerMin, // 70 minutes ago, in uSec
endTime: now_uSec - 71 * kUsecPerMin, // 1 minute later
state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED,
currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0,
guid: "a1cbD23e4Fg5"
};
target: "fakefile-1-hour-10-minutes"
});
download.startTime = new Date(now_mSec - 70 * kMsecPerMin), // 70 minutes ago
download.canceled = true;
publicList.add(download);
try {
for (let prop in data)
stmt.params[prop] = data[prop];
stmt.execute();
}
finally {
stmt.reset();
}
// Add within-2-hour download
data = {
id: "5555552",
name: "fakefile-2-hour",
download = yield Downloads.createDownload({
source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440",
target: "fakefile-2-hour",
startTime: now_uSec - 90 * kUsecPerMin, // 90 minutes ago, in uSec
endTime: now_uSec - 89 * kUsecPerMin, // 1 minute later
state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED,
currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0,
guid: "b1aDc23eFg54"
};
target: "fakefile-2-hour"
});
download.startTime = new Date(now_mSec - 90 * kMsecPerMin), // 90 minutes ago
download.canceled = true;
publicList.add(download);
try {
for (let prop in data)
stmt.params[prop] = data[prop];
stmt.execute();
}
finally {
stmt.reset();
}
// Add 2-hour-10-minutes download
data = {
id: "5555557",
name: "fakefile-2-hour-10-minutes",
download = yield Downloads.createDownload({
source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169",
target: "fakefile-2-hour-10-minutes",
startTime: now_uSec - 130 * kUsecPerMin, // 130 minutes ago, in uSec
endTime: now_uSec - 131 * kUsecPerMin, // 1 minute later
state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED,
currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0,
guid: "z1bcD23eF4g5"
};
target: "fakefile-2-hour-10-minutes"
});
download.startTime = new Date(now_mSec - 130 * kMsecPerMin), // 130 minutes ago
download.canceled = true;
publicList.add(download);
try {
for (let prop in data)
stmt.params[prop] = data[prop];
stmt.execute();
}
finally {
stmt.reset();
}
// Add within-4-hour download
data = {
id: "5555553",
name: "fakefile-4-hour",
download = yield Downloads.createDownload({
source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440",
target: "fakefile-4-hour",
startTime: now_uSec - 180 * kUsecPerMin, // 180 minutes ago, in uSec
endTime: now_uSec - 179 * kUsecPerMin, // 1 minute later
state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED,
currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0,
guid: "zzzcD23eF4g5"
};
try {
for (let prop in data)
stmt.params[prop] = data[prop];
stmt.execute();
}
finally {
stmt.reset();
}
// Add 4-hour-10-minutes download
data = {
id: "5555558",
name: "fakefile-4-hour-10-minutes",
target: "fakefile-4-hour"
});
download.startTime = new Date(now_mSec - 180 * kMsecPerMin), // 180 minutes ago
download.canceled = true;
publicList.add(download);
download = yield Downloads.createDownload({
source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169",
target: "fakefile-4-hour-10-minutes",
startTime: now_uSec - 250 * kUsecPerMin, // 250 minutes ago, in uSec
endTime: now_uSec - 251 * kUsecPerMin, // 1 minute later
state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED,
currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0,
guid: "z1bzz23eF4gz"
};
try {
for (let prop in data)
stmt.params[prop] = data[prop];
stmt.execute();
}
finally {
stmt.reset();
}
target: "fakefile-4-hour-10-minutes"
});
download.startTime = new Date(now_mSec - 250 * kMsecPerMin), // 250 minutes ago
download.canceled = true;
publicList.add(download);
// Add "today" download
let today = new Date();
today.setHours(0);
today.setMinutes(0);
today.setSeconds(1);
data = {
id: "5555554",
name: "fakefile-today",
download = yield Downloads.createDownload({
source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440",
target: "fakefile-today",
startTime: today.getTime() * 1000, // 12:00:30am this morning, in uSec
endTime: (today.getTime() + 1000) * 1000, // 1 second later
state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED,
currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0,
guid: "ffffD23eF4g5"
};
try {
for (let prop in data)
stmt.params[prop] = data[prop];
stmt.execute();
}
finally {
stmt.reset();
}
target: "fakefile-today"
});
download.startTime = today, // 12:00:01 AM this morning
download.canceled = true;
publicList.add(download);
// Add "before today" download
let lastYear = new Date();
lastYear.setFullYear(lastYear.getFullYear() - 1);
data = {
id: "5555550",
name: "fakefile-old",
download = yield Downloads.createDownload({
source: "https://bugzilla.mozilla.org/show_bug.cgi?id=453440",
target: "fakefile-old",
startTime: lastYear.getTime() * 1000, // 1 year ago, in uSec
endTime: (lastYear.getTime() + 1000) * 1000, // 1 second later
state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED,
currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0,
guid: "ggggg23eF4g5"
};
try {
for (let prop in data)
stmt.params[prop] = data[prop];
stmt.execute();
}
finally {
stmt.finalize();
}
target: "fakefile-old"
});
download.startTime = lastYear,
download.canceled = true;
publicList.add(download);
// Confirm everything worked
ok(downloadExists(5555550), "Pretend download for everything case should exist");
ok(downloadExists(5555555), "Pretend download for 10-minutes case should exist");
ok(downloadExists(5555551), "Pretend download for 1-hour case should exist");
ok(downloadExists(5555556), "Pretend download for 1-hour-10-minutes case should exist");
ok(downloadExists(5555552), "Pretend download for 2-hour case should exist");
ok(downloadExists(5555557), "Pretend download for 2-hour-10-minutes case should exist");
ok(downloadExists(5555553), "Pretend download for 4-hour case should exist");
ok(downloadExists(5555558), "Pretend download for 4-hour-10-minutes case should exist");
ok(downloadExists(5555554), "Pretend download for Today case should exist");
let downloads = yield publicList.getAll();
is(downloads.length, 9, "9 Pretend downloads added");
ok((yield downloadExists(publicList, "fakefile-old")), "Pretend download for everything case should exist");
ok((yield downloadExists(publicList, "fakefile-10-minutes")), "Pretend download for 10-minutes case should exist");
ok((yield downloadExists(publicList, "fakefile-1-hour")), "Pretend download for 1-hour case should exist");
ok((yield downloadExists(publicList, "fakefile-1-hour-10-minutes")), "Pretend download for 1-hour-10-minutes case should exist");
ok((yield downloadExists(publicList, "fakefile-2-hour")), "Pretend download for 2-hour case should exist");
ok((yield downloadExists(publicList, "fakefile-2-hour-10-minutes")), "Pretend download for 2-hour-10-minutes case should exist");
ok((yield downloadExists(publicList, "fakefile-4-hour")), "Pretend download for 4-hour case should exist");
ok((yield downloadExists(publicList, "fakefile-4-hour-10-minutes")), "Pretend download for 4-hour-10-minutes case should exist");
ok((yield downloadExists(publicList, "fakefile-today")), "Pretend download for Today case should exist");
}
/**
@ -791,18 +717,12 @@ function setupDownloads() {
* @param aID
* The ids of the downloads to check.
*/
function downloadExists(aID)
function downloadExists(list, path)
{
let db = dm.DBConnection;
let stmt = db.createStatement(
"SELECT * " +
"FROM moz_downloads " +
"WHERE id = :id"
);
stmt.params.id = aID;
var rows = stmt.executeStep();
stmt.finalize();
return rows;
return Task.spawn(function() {
let listArray = yield list.getAll();
throw new Task.Result(listArray.some(i => i.target.path == path));
});
}
function isToday(aDate) {

View File

@ -21,18 +21,18 @@ Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
"resource://gre/modules/FormHistory.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
"resource://gre/modules/Downloads.jsm");
let tempScope = {};
Cc["@mozilla.org/moz/jssubscript-loader;1"].getService(Ci.mozIJSSubScriptLoader)
.loadSubScript("chrome://browser/content/sanitize.js", tempScope);
let Sanitizer = tempScope.Sanitizer;
const dm = Cc["@mozilla.org/download-manager;1"].
getService(Ci.nsIDownloadManager);
const kMsecPerMin = 60 * 1000;
const kUsecPerMin = 60 * 1000000;
let formEntries;
let formEntries, downloadIDs, olderDownloadIDs;
// Add tests here. Each is a function that's called by doNextTest().
var gAllTests = [
@ -92,6 +92,23 @@ var gAllTests = [
});
},
function () {
// Add downloads (within the past hour).
Task.spawn(function () {
downloadIDs = [];
for (let i = 0; i < 5; i++) {
yield addDownloadWithMinutesAgo(downloadIDs, i);
}
// Add downloads (over an hour ago).
olderDownloadIDs = [];
for (let i = 0; i < 5; i++) {
yield addDownloadWithMinutesAgo(olderDownloadIDs, 61 + i);
}
doNextTest();
}).then(null, Components.utils.reportError);
},
/**
* Ensures that the combined history-downloads checkbox clears both history
* visits and downloads when checked; the dialog respects simple timespan.
@ -115,16 +132,6 @@ var gAllTests = [
}
addVisits(places, function() {
// Add downloads (within the past hour).
let downloadIDs = [];
for (let i = 0; i < 5; i++) {
downloadIDs.push(addDownloadWithMinutesAgo(i));
}
// Add downloads (over an hour ago).
let olderDownloadIDs = [];
for (let i = 0; i < 5; i++) {
olderDownloadIDs.push(addDownloadWithMinutesAgo(61 + i));
}
let totalHistoryVisits = uris.length + olderURIs.length;
let wh = new WindowHelper();
@ -146,16 +153,16 @@ var gAllTests = [
wh.onunload = function () {
// History visits and downloads within one hour should be cleared.
yield promiseHistoryClearedState(uris, true);
ensureDownloadsClearedState(downloadIDs, true);
yield ensureDownloadsClearedState(downloadIDs, true);
// Visits and downloads > 1 hour should still exist.
yield promiseHistoryClearedState(olderURIs, false);
ensureDownloadsClearedState(olderDownloadIDs, false);
yield ensureDownloadsClearedState(olderDownloadIDs, false);
// OK, done, cleanup after ourselves.
yield blankSlate();
yield promiseHistoryClearedState(olderURIs, true);
ensureDownloadsClearedState(olderDownloadIDs, true);
yield ensureDownloadsClearedState(olderDownloadIDs, true);
};
wh.open();
});
@ -178,6 +185,18 @@ var gAllTests = [
iter.next();
},
function () {
// Add downloads (within the past hour).
Task.spawn(function () {
downloadIDs = [];
for (let i = 0; i < 5; i++) {
yield addDownloadWithMinutesAgo(downloadIDs, i);
}
doNextTest();
}).then(null, Components.utils.reportError);
},
/**
* Ensures that the combined history-downloads checkbox removes neither
* history visits nor downloads when not checked.
@ -194,11 +213,6 @@ var gAllTests = [
}
addVisits(places, function() {
let downloadIDs = [];
for (let i = 0; i < 5; i++) {
downloadIDs.push(addDownloadWithMinutesAgo(i));
}
let wh = new WindowHelper();
wh.onload = function () {
is(this.isWarningPanelVisible(), false,
@ -224,7 +238,7 @@ var gAllTests = [
wh.onunload = function () {
// Of the three only form entries should be cleared.
yield promiseHistoryClearedState(uris, false);
ensureDownloadsClearedState(downloadIDs, false);
yield ensureDownloadsClearedState(downloadIDs, false);
formEntries.forEach(function (entry) {
let exists = yield formNameExists(entry);
@ -234,7 +248,7 @@ var gAllTests = [
// OK, done, cleanup after ourselves.
yield blankSlate();
yield promiseHistoryClearedState(uris, true);
ensureDownloadsClearedState(downloadIDs, true);
yield ensureDownloadsClearedState(downloadIDs, true);
};
wh.open();
});
@ -639,15 +653,12 @@ var gAllTests = [
}
];
// Used as the download database ID for a new download. Incremented for each
// new download. See addDownloadWithMinutesAgo().
var gDownloadId = 5555551;
// Index in gAllTests of the test currently being run. Incremented for each
// test run. See doNextTest().
var gCurrTest = 0;
var now_uSec = Date.now() * 1000;
let now_mSec = Date.now();
let now_uSec = now_mSec * 1000;
///////////////////////////////////////////////////////////////////////////////
@ -847,7 +858,7 @@ WindowHelper.prototype = {
if (wh.onunload) {
Task.spawn(wh.onunload).then(function() {
waitForAsyncUpdates(doNextTest);
});
}).then(null, Components.utils.reportError);
} else {
waitForAsyncUpdates(doNextTest);
}
@ -900,40 +911,23 @@ WindowHelper.prototype = {
* @param aMinutesAgo
* The download will be downloaded this many minutes ago
*/
function addDownloadWithMinutesAgo(aMinutesAgo) {
function addDownloadWithMinutesAgo(aExpectedPathList, aMinutesAgo) {
let publicList = yield Downloads.getPublicDownloadList();
let name = "fakefile-" + aMinutesAgo + "-minutes-ago";
let data = {
id: gDownloadId,
name: name,
source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169",
target: name,
startTime: now_uSec - (aMinutesAgo * kUsecPerMin),
endTime: now_uSec - ((aMinutesAgo + 1) * kUsecPerMin),
state: Ci.nsIDownloadManager.DOWNLOAD_FINISHED,
currBytes: 0, maxBytes: -1, preferredAction: 0, autoResume: 0
};
let download = yield Downloads.createDownload({
source: "https://bugzilla.mozilla.org/show_bug.cgi?id=480169",
target: name
});
download.startTime = new Date(now_mSec - (aMinutesAgo * kMsecPerMin));
download.canceled = true;
publicList.add(download);
let db = dm.DBConnection;
let stmt = db.createStatement(
"INSERT INTO moz_downloads (id, name, source, target, startTime, endTime, " +
"state, currBytes, maxBytes, preferredAction, autoResume) " +
"VALUES (:id, :name, :source, :target, :startTime, :endTime, :state, " +
":currBytes, :maxBytes, :preferredAction, :autoResume)");
try {
for (let prop in data) {
stmt.params[prop] = data[prop];
}
stmt.execute();
}
finally {
stmt.reset();
}
is(downloadExists(gDownloadId), true,
"Sanity check: download " + gDownloadId +
ok((yield downloadExists(name)),
"Sanity check: download " + name +
" should exist after creating it");
return gDownloadId++;
aExpectedPathList.push(name);
}
/**
@ -984,15 +978,37 @@ function formNameExists(name)
*/
function blankSlate() {
PlacesUtils.bhistory.removeAllPages();
dm.cleanUp();
// The promise is resolved only when removing both downloads and form history are done.
let deferred = Promise.defer();
let formHistoryDone = false, downloadsDone = false;
Task.spawn(function deleteAllDownloads() {
let publicList = yield Downloads.getPublicDownloadList();
let downloads = yield publicList.getAll();
for (let download of downloads) {
publicList.remove(download);
yield download.finalize(true);
}
downloadsDone = true;
if (formHistoryDone) {
deferred.resolve();
}
}).then(null, Components.utils.reportError);
FormHistory.update({ op: "remove" },
{ handleError: function (error) {
do_throw("Error occurred updating form history: " + error);
deferred.reject(error);
},
handleCompletion: function (reason) { if (!reason) deferred.resolve(); }
handleCompletion: function (reason) {
if (!reason) {
formHistoryDone = true;
if (downloadsDone) {
deferred.resolve();
}
}
}
});
return deferred.promise;
}
@ -1012,24 +1028,19 @@ function boolPrefIs(aPrefName, aExpectedVal, aMsg) {
}
/**
* Checks to see if the download with the specified ID exists.
* Checks to see if the download with the specified path exists.
*
* @param aID
* The ID of the download to check
* @param aPath
* The path of the download to check
* @return True if the download exists, false otherwise
*/
function downloadExists(aID)
function downloadExists(aPath)
{
let db = dm.DBConnection;
let stmt = db.createStatement(
"SELECT * " +
"FROM moz_downloads " +
"WHERE id = :id"
);
stmt.params.id = aID;
let rows = stmt.executeStep();
stmt.finalize();
return !!rows;
return Task.spawn(function() {
let publicList = yield Downloads.getPublicDownloadList();
let listArray = yield publicList.getAll();
throw new Task.Result(listArray.some(i => i.target.path == aPath));
});
}
/**
@ -1059,7 +1070,7 @@ function doNextTest() {
function ensureDownloadsClearedState(aDownloadIDs, aShouldBeCleared) {
let niceStr = aShouldBeCleared ? "no longer" : "still";
aDownloadIDs.forEach(function (id) {
is(downloadExists(id), !aShouldBeCleared,
is((yield downloadExists(id)), !aShouldBeCleared,
"download " + id + " should " + niceStr + " exist");
});
}

View File

@ -14,6 +14,7 @@ MOCHITEST_BROWSER_FILES = \
browser_newtab_block.js \
browser_newtab_disable.js \
browser_newtab_drag_drop.js \
browser_newtab_drag_drop_ext.js \
browser_newtab_drop_preview.js \
browser_newtab_focus.js \
browser_newtab_reset.js \

View File

@ -71,47 +71,4 @@ function runTests() {
yield simulateDrop(0, 4);
checkGrid("3,1p,2p,4,0p,5p,6,7,8");
// drag a new site onto the very first cell
yield setLinks("0,1,2,3,4,5,6,7,8");
setPinnedLinks(",,,,,,,7,8");
yield addNewTabPageTab();
checkGrid("0,1,2,3,4,5,6,7p,8p");
yield simulateExternalDrop(0);
checkGrid("99p,0,1,2,3,4,5,7p,8p");
// drag a new site onto the grid and make sure that pinned cells don't get
// pushed out
yield setLinks("0,1,2,3,4,5,6,7,8");
setPinnedLinks(",,,,,,,7,8");
yield addNewTabPageTab();
checkGrid("0,1,2,3,4,5,6,7p,8p");
yield simulateExternalDrop(7);
checkGrid("0,1,2,3,4,5,7p,99p,8p");
// drag a new site beneath a pinned cell and make sure the pinned cell is
// not moved
yield setLinks("0,1,2,3,4,5,6,7,8");
setPinnedLinks(",,,,,,,,8");
yield addNewTabPageTab();
checkGrid("0,1,2,3,4,5,6,7,8p");
yield simulateExternalDrop(7);
checkGrid("0,1,2,3,4,5,6,99p,8p");
// drag a new site onto a block of pinned sites and make sure they're shifted
// around accordingly
yield setLinks("0,1,2,3,4,5,6,7,8");
setPinnedLinks("0,1,2,,,,,,");
yield addNewTabPageTab();
checkGrid("0p,1p,2p");
yield simulateExternalDrop(1);
checkGrid("0p,99p,1p,2p,3,4,5,6,7");
}

View File

@ -0,0 +1,55 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/*
* These tests make sure that dragging and dropping sites works as expected.
* Sites contained in the grid need to shift around to indicate the result
* of the drag-and-drop operation. If the grid is full and we're dragging
* a new site into it another one gets pushed out.
* This is a continuation of browser_newtab_drag_drop.js
* to decrease test run time, focusing on external sites.
*/
function runTests() {
// drag a new site onto the very first cell
yield setLinks("0,1,2,3,4,5,6,7,8");
setPinnedLinks(",,,,,,,7,8");
yield addNewTabPageTab();
checkGrid("0,1,2,3,4,5,6,7p,8p");
yield simulateExternalDrop(0);
checkGrid("99p,0,1,2,3,4,5,7p,8p");
// drag a new site onto the grid and make sure that pinned cells don't get
// pushed out
yield setLinks("0,1,2,3,4,5,6,7,8");
setPinnedLinks(",,,,,,,7,8");
yield addNewTabPageTab();
checkGrid("0,1,2,3,4,5,6,7p,8p");
yield simulateExternalDrop(7);
checkGrid("0,1,2,3,4,5,7p,99p,8p");
// drag a new site beneath a pinned cell and make sure the pinned cell is
// not moved
yield setLinks("0,1,2,3,4,5,6,7,8");
setPinnedLinks(",,,,,,,,8");
yield addNewTabPageTab();
checkGrid("0,1,2,3,4,5,6,7,8p");
yield simulateExternalDrop(7);
checkGrid("0,1,2,3,4,5,6,99p,8p");
// drag a new site onto a block of pinned sites and make sure they're shifted
// around accordingly
yield setLinks("0,1,2,3,4,5,6,7,8");
setPinnedLinks("0,1,2,,,,,,");
yield addNewTabPageTab();
checkGrid("0p,1p,2p");
yield simulateExternalDrop(1);
checkGrid("0p,99p,1p,2p,3,4,5,6,7");
}

View File

@ -30,6 +30,7 @@ MOCHITEST_BROWSER_FILES = \
browser_social_chatwindow_resize.js \
browser_social_chatwindowfocus.js \
browser_social_multiprovider.js \
browser_social_multiworker.js \
browser_social_errorPage.js \
browser_social_window.js \
social_activate.html \

View File

@ -2,17 +2,71 @@
* 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/. */
let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
let manifests = [
{
name: "provider@example.com",
origin: "https://example.com",
sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html?example.com",
workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js",
iconURL: "chrome://branding/content/icon48.png"
},
{
name: "provider@test1",
origin: "https://test1.example.com",
sidebarURL: "https://test1.example.com/browser/browser/base/content/test/social/social_sidebar.html?test1",
workerURL: "https://test1.example.com/browser/browser/base/content/test/social/social_worker.js",
iconURL: "chrome://branding/content/icon48.png"
},
{
name: "provider@test2",
origin: "https://test2.example.com",
sidebarURL: "https://test2.example.com/browser/browser/base/content/test/social/social_sidebar.html?test2",
workerURL: "https://test2.example.com/browser/browser/base/content/test/social/social_worker.js",
iconURL: "chrome://branding/content/icon48.png"
}
];
let chatId = 0;
function openChat(provider, callback) {
let chatUrl = provider.origin + "/browser/browser/base/content/test/social/social_chat.html";
let port = provider.getWorkerPort();
port.onmessage = function(e) {
if (e.data.topic == "got-chatbox-message") {
port.close();
callback();
}
}
let url = chatUrl + "?" + (chatId++);
port.postMessage({topic: "test-init"});
port.postMessage({topic: "test-worker-chat", data: url});
gURLsNotRemembered.push(url);
}
function waitPrefChange(cb) {
Services.prefs.addObserver("social.enabled", function prefObserver(subject, topic, data) {
Services.prefs.removeObserver("social.enabled", prefObserver);
executeSoon(cb);
}, false);
}
function setWorkerMode(multiple, cb) {
waitPrefChange(function() {
if (multiple)
Services.prefs.setBoolPref("social.allowMultipleWorkers", true);
else
Services.prefs.clearUserPref("social.allowMultipleWorkers");
waitPrefChange(cb);
Social.enabled = true;
});
Social.enabled = false;
}
function test() {
requestLongerTimeout(2); // only debug builds seem to need more time...
waitForExplicitFinish();
let manifest = { // normal provider
name: "provider 1",
origin: "https://example.com",
sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html",
workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js",
iconURL: "https://example.com/browser/browser/base/content/test/moz.png"
};
let oldwidth = window.outerWidth; // we futz with these, so we restore them
let oldleft = window.screenX;
window.moveTo(0, window.screenY)
@ -21,7 +75,7 @@ function test() {
ok(chats.children.length == 0, "no chatty children left behind");
cb();
};
runSocialTestWithProvider(manifest, function (finishcb) {
runSocialTestWithProvider(manifests, function (finishcb) {
runSocialTests(tests, undefined, postSubTest, function() {
window.moveTo(oldleft, window.screenY)
window.resizeTo(oldwidth, window.outerHeight);
@ -147,7 +201,7 @@ var tests = {
maybeOpenAnother();
},
testWorkerChatWindow: function(next) {
const chatUrl = "https://example.com/browser/browser/base/content/test/social/social_chat.html";
const chatUrl = Social.provider.origin + "/browser/browser/base/content/test/social/social_chat.html";
let chats = document.getElementById("pinnedchats");
let port = Social.provider.getWorkerPort();
ok(port, "provider has a port");
@ -384,7 +438,7 @@ var tests = {
testSecondTopLevelWindow: function(next) {
// Bug 817782 - check chats work in new top-level windows.
const chatUrl = "https://example.com/browser/browser/base/content/test/social/social_chat.html";
const chatUrl = Social.provider.origin + "/browser/browser/base/content/test/social/social_chat.html";
let port = Social.provider.getWorkerPort();
let secondWindow;
port.onmessage = function(e) {
@ -407,23 +461,9 @@ var tests = {
testChatWindowChooser: function(next) {
// Tests that when a worker creates a chat, it is opened in the correct
// window.
const chatUrl = "https://example.com/browser/browser/base/content/test/social/social_chat.html";
let chatId = 1;
let port = Social.provider.getWorkerPort();
port.postMessage({topic: "test-init"});
function openChat(callback) {
port.onmessage = function(e) {
if (e.data.topic == "got-chatbox-message")
callback();
}
let url = chatUrl + "?" + (chatId++);
port.postMessage({topic: "test-worker-chat", data: url});
}
// open a chat (it will open in the main window)
ok(!window.SocialChatBar.hasChats, "first window should start with no chats");
openChat(function() {
openChat(Social.provider, function() {
ok(window.SocialChatBar.hasChats, "first window has the chat");
// create a second window - this will be the "most recent" and will
// therefore be the window that hosts the new chat (see bug 835111)
@ -431,27 +471,55 @@ var tests = {
secondWindow.addEventListener("load", function loadListener() {
secondWindow.removeEventListener("load", loadListener);
ok(!secondWindow.SocialChatBar.hasChats, "second window has no chats");
openChat(function() {
openChat(Social.provider, function() {
ok(secondWindow.SocialChatBar.hasChats, "second window now has chats");
is(window.SocialChatBar.chatbar.childElementCount, 1, "first window still has 1 chat");
window.SocialChatBar.chatbar.removeAll();
// now open another chat - it should still open in the second.
openChat(function() {
openChat(Social.provider, function() {
ok(!window.SocialChatBar.hasChats, "first window has no chats");
ok(secondWindow.SocialChatBar.hasChats, "second window has a chat");
secondWindow.close();
port.close();
next();
});
});
})
});
},
testMultipleProviderChat: function(next) {
// while pref'd off, we need to set the worker mode to multiple providers
setWorkerMode(true, function() {
// test incomming chats from all providers
openChat(Social.providers[0], function() {
openChat(Social.providers[1], function() {
openChat(Social.providers[2], function() {
let chats = document.getElementById("pinnedchats");
waitForCondition(function() chats.children.length == Social.providers.length,
function() {
ok(true, "one chat window per provider opened");
// test logout of a single provider
let provider = Social.providers[0];
let port = provider.getWorkerPort();
port.postMessage({topic: "test-logout"});
waitForCondition(function() chats.children.length == Social.providers.length - 1,
function() {
port.close();
chats.removeAll();
ok(!chats.selectedChat, "chats are all closed");
setWorkerMode(false, next);
},
"chat window didn't close");
}, "chat windows did not open");
});
});
});
});
},
// XXX - note this must be the last test until we restore the login state
// between tests...
testCloseOnLogout: function(next) {
const chatUrl = "https://example.com/browser/browser/base/content/test/social/social_chat.html";
const chatUrl = Social.provider.origin + "/browser/browser/base/content/test/social/social_chat.html";
let port = Social.provider.getWorkerPort();
ok(port, "provider has a port");
let opened = false;

View File

@ -0,0 +1,101 @@
/* 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/. */
function test() {
waitForExplicitFinish();
Services.prefs.setBoolPref("social.allowMultipleWorkers", true);
runSocialTestWithProvider(gProviders, function (finishcb) {
runSocialTests(tests, undefined, undefined, function() {
Services.prefs.clearUserPref("social.allowMultipleWorkers");
finishcb();
});
});
}
let gProviders = [
{
name: "provider 1",
origin: "https://example.com",
sidebarURL: "https://example.com/browser/browser/base/content/test/social/social_sidebar.html?provider1",
workerURL: "https://example.com/browser/browser/base/content/test/social/social_worker.js",
iconURL: "chrome://branding/content/icon48.png"
},
{
name: "provider 2",
origin: "https://test1.example.com",
sidebarURL: "https://test1.example.com/browser/browser/base/content/test/social/social_sidebar.html?provider2",
workerURL: "https://test1.example.com/browser/browser/base/content/test/social/social_worker.js",
iconURL: "chrome://branding/content/icon48.png"
}
];
var tests = {
testWorkersAlive: function(next) {
// verify we can get a message from all providers that are enabled
let messageReceived = 0;
function oneWorkerTest(provider) {
let port = provider.getWorkerPort();
port.onmessage = function (e) {
let topic = e.data.topic;
switch (topic) {
case "test-init-done":
ok(true, "got message from provider " + provider.name);
port.close();
messageReceived++;
break;
}
};
port.postMessage({topic: "test-init"});
}
for (let p of Social.providers) {
oneWorkerTest(p);
}
waitForCondition(function() messageReceived == Social.providers.length,
next, "received messages from all workers");
},
testWorkerDisabling: function(next) {
Social.enabled = false;
is(Social.providers.length, gProviders.length, "providers still available");
for (let p of Social.providers) {
ok(!p.enabled, "provider disabled");
ok(!p.getWorkerPort(), "worker disabled");
}
next();
},
testSingleWorkerEnabling: function(next) {
// test that only one worker is enabled when we limit workers
Services.prefs.setBoolPref("social.allowMultipleWorkers", false);
Social.enabled = true;
for (let p of Social.providers) {
if (p == Social.provider) {
ok(p.enabled, "primary provider enabled");
let port = p.getWorkerPort();
ok(port, "primary worker enabled");
port.close();
} else {
ok(!p.enabled, "secondary provider is not enabled");
ok(!p.getWorkerPort(), "secondary worker disabled");
}
}
next();
},
testMultipleWorkerEnabling: function(next) {
// test that all workers are enabled when we allow multiple workers
Social.enabled = false;
Services.prefs.setBoolPref("social.allowMultipleWorkers", true);
Social.enabled = true;
for (let p of Social.providers) {
ok(p.enabled, "provider enabled");
let port = p.getWorkerPort();
ok(port, "worker enabled");
port.close();
}
next();
}
}

View File

@ -417,8 +417,8 @@ function get3ChatsForCollapsing(mode, cb) {
function makeChat(mode, uniqueid, cb) {
info("making a chat window '" + uniqueid +"'");
const chatUrl = "https://example.com/browser/browser/base/content/test/social/social_chat.html";
let provider = Social.provider;
const chatUrl = provider.origin + "/browser/browser/base/content/test/social/social_chat.html";
let isOpened = window.SocialChatBar.openChat(provider, chatUrl + "?id=" + uniqueid, function(chat) {
info("chat window has opened");
// we can't callback immediately or we might close the chat during

View File

@ -7,39 +7,33 @@
* Make sure the downloads panel can display items in the right order and
* contains the expected data.
*/
function gen_test()
function test_task()
{
// Display one of each download state.
const DownloadData = [
{ endTime: 1180493839859239, state: nsIDM.DOWNLOAD_NOTSTARTED },
{ endTime: 1180493839859238, state: nsIDM.DOWNLOAD_DOWNLOADING },
{ endTime: 1180493839859237, state: nsIDM.DOWNLOAD_PAUSED },
{ endTime: 1180493839859236, state: nsIDM.DOWNLOAD_SCANNING },
{ endTime: 1180493839859235, state: nsIDM.DOWNLOAD_QUEUED },
{ endTime: 1180493839859234, state: nsIDM.DOWNLOAD_FINISHED },
{ endTime: 1180493839859233, state: nsIDM.DOWNLOAD_FAILED },
{ endTime: 1180493839859232, state: nsIDM.DOWNLOAD_CANCELED },
{ endTime: 1180493839859231, state: nsIDM.DOWNLOAD_BLOCKED_PARENTAL },
{ endTime: 1180493839859230, state: nsIDM.DOWNLOAD_DIRTY },
{ endTime: 1180493839859229, state: nsIDM.DOWNLOAD_BLOCKED_POLICY },
{ state: nsIDM.DOWNLOAD_NOTSTARTED },
{ state: nsIDM.DOWNLOAD_PAUSED },
{ state: nsIDM.DOWNLOAD_FINISHED },
{ state: nsIDM.DOWNLOAD_FAILED },
{ state: nsIDM.DOWNLOAD_CANCELED },
];
// For testing purposes, show all the download items at once.
var originalCountLimit = DownloadsView.kItemCountLimit;
DownloadsView.kItemCountLimit = DownloadData.length;
registerCleanupFunction(function () {
DownloadsView.kItemCountLimit = originalCountLimit;
});
try {
// Ensure that state is reset in case previous tests didn't finish.
for (let yy in gen_resetState(DownloadsCommon.getData(window))) yield undefined;
yield task_resetState();
// For testing purposes, show all the download items at once.
var originalCountLimit = DownloadsView.kItemCountLimit;
DownloadsView.kItemCountLimit = DownloadData.length;
registerCleanupFunction(function () {
DownloadsView.kItemCountLimit = originalCountLimit;
});
// Populate the downloads database with the data required by this test.
for (let yy in gen_addDownloadRows(DownloadData)) yield undefined;
yield task_addDownloads(DownloadData);
// Open the user interface and wait for data to be fully loaded.
for (let yy in gen_openPanel(DownloadsCommon.getData(window))) yield undefined;
yield task_openPanel();
// Test item data and count. This also tests the ordering of the display.
let richlistbox = document.getElementById("downloadsListBox");
@ -47,16 +41,14 @@ function gen_test()
is(richlistbox.children.length, DownloadData.length,
"There is the correct number of richlistitems");
*/
for (let i = 0; i < richlistbox.children.length; i++) {
let element = richlistbox.children[i];
let itemCount = richlistbox.children.length;
for (let i = 0; i < itemCount; i++) {
let element = richlistbox.children[itemCount - i - 1];
let dataItem = new DownloadsViewItemController(element).dataItem;
is(dataItem.target, DownloadData[i].name, "Download names match up");
is(dataItem.state, DownloadData[i].state, "Download states match up");
is(dataItem.file, DownloadData[i].target, "Download targets match up");
is(dataItem.uri, DownloadData[i].source, "Download sources match up");
}
} finally {
// Clean up when the test finishes.
for (let yy in gen_resetState(DownloadsCommon.getData(window))) yield undefined;
yield task_resetState();
}
}

View File

@ -8,19 +8,19 @@
* download it notices. All subsequent downloads, even across sessions, should
* not open the panel automatically.
*/
function gen_test()
function test_task()
{
try {
// Ensure that state is reset in case previous tests didn't finish.
for (let yy in gen_resetState(DownloadsCommon.getData(window))) yield undefined;
yield task_resetState();
// With this set to false, we should automatically open the panel
// the first time a download is started.
// With this set to false, we should automatically open the panel the first
// time a download is started.
DownloadsCommon.getData(window).panelHasShownBefore = false;
prepareForPanelOpen();
let promise = promisePanelOpened();
DownloadsCommon.getData(window)._notifyDownloadEvent("start");
yield undefined;
yield promise;
// If we got here, that means the panel opened.
DownloadsPanel.hidePanel();
@ -28,29 +28,26 @@ function gen_test()
ok(DownloadsCommon.getData(window).panelHasShownBefore,
"Should have recorded that the panel was opened on a download.")
// Next, make sure that if we start another download, we don't open
// the panel automatically.
panelShouldNotOpen();
DownloadsCommon.getData(window)._notifyDownloadEvent("start");
yield waitFor(2);
} catch(e) {
ok(false, e);
// Next, make sure that if we start another download, we don't open the
// panel automatically.
let originalOnPopupShown = DownloadsPanel.onPopupShown;
DownloadsPanel.onPopupShown = function () {
originalOnPopupShown.apply(this, arguments);
ok(false, "Should not have opened the downloads panel.");
};
try {
DownloadsCommon.getData(window)._notifyDownloadEvent("start");
// Wait 2 seconds to ensure that the panel does not open.
let deferTimeout = Promise.defer();
setTimeout(deferTimeout.resolve, 2000);
yield deferTimeout.promise;
} finally {
DownloadsPanel.onPopupShown = originalOnPopupShown;
}
} finally {
// Clean up when the test finishes.
for (let yy in gen_resetState(DownloadsCommon.getData(window))) yield undefined;
yield task_resetState();
}
}
/**
* Call this to record a test failure for the next time the downloads panel
* opens.
*/
function panelShouldNotOpen()
{
// Hook to wait until the test data has been loaded.
let originalOnViewLoadCompleted = DownloadsPanel.onViewLoadCompleted;
DownloadsPanel.onViewLoadCompleted = function () {
DownloadsPanel.onViewLoadCompleted = originalOnViewLoadCompleted;
ok(false, "Should not have opened the downloads panel.");
};
}

View File

@ -10,10 +10,16 @@
////////////////////////////////////////////////////////////////////////////////
//// Globals
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Downloads",
"resource://gre/modules/Downloads.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DownloadsCommon",
"resource:///modules/DownloadsCommon.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
"resource://gre/modules/FileUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
const nsIDM = Ci.nsIDownloadManager;
let gTestTargetFile = FileUtils.getFile("TmpD", ["dm-ui-test.file"]);
@ -22,253 +28,85 @@ registerCleanupFunction(function () {
gTestTargetFile.remove(false);
});
/**
* This objects contains a property for each column in the downloads table.
*/
let gDownloadRowTemplate = {
name: "test-download.txt",
source: "http://www.example.com/test-download.txt",
target: NetUtil.newURI(gTestTargetFile).spec,
startTime: 1180493839859230,
endTime: 1180493839859234,
state: nsIDM.DOWNLOAD_FINISHED,
currBytes: 0,
maxBytes: -1,
preferredAction: 0,
autoResume: 0
};
////////////////////////////////////////////////////////////////////////////////
//// Infrastructure
// All test are run through the test runner.
function test()
{
testRunner.runTest(this.gen_test);
waitForExplicitFinish();
Task.spawn(test_task).then(null, ex => ok(false, ex)).then(finish);
}
/**
* Runs a browser-chrome test defined through a generator function.
*
* This object is a singleton, initialized automatically when this script is
* included. Every browser-chrome test file includes a new copy of this object.
*/
var testRunner = {
_testIterator: null,
_lastEventResult: undefined,
_testRunning: false,
_eventRaised: false,
// --- Main test runner ---
/**
* Runs the test described by the provided generator function asynchronously.
*
* Calling yield in the generator will cause it to wait until continueTest is
* called. The parameter provided to continueTest will be the return value of
* the yield operator.
*
* @param aGenerator
* Test generator function. The function will be called with no
* arguments to retrieve its iterator.
*/
runTest: function TR_runTest(aGenerator) {
waitForExplicitFinish();
testRunner._testIterator = aGenerator();
testRunner.continueTest();
},
/**
* Continues the currently running test.
*
* @param aEventResult
* This will be the return value of the yield operator in the test.
*/
continueTest: function TR_continueTest(aEventResult) {
// Store the last event result, or set it to undefined.
testRunner._lastEventResult = aEventResult;
// Never reenter the main loop, but notify that the event has been raised.
if (testRunner._testRunning) {
testRunner._eventRaised = true;
return;
}
// Enter the main iteration loop.
testRunner._testRunning = true;
try {
do {
// Call the iterator, but don't leave the loop if the expected event is
// raised during the execution of the generator.
testRunner._eventRaised = false;
testRunner._testIterator.send(testRunner._lastEventResult);
} while (testRunner._eventRaised);
}
catch (e) {
// This block catches exceptions raised by the generator, including the
// normal StopIteration exception. Unexpected exceptions are reported as
// test failures.
if (!(e instanceof StopIteration))
ok(false, e);
// In any case, stop the tests in this file.
finish();
}
// Wait for the next event or finish.
testRunner._testRunning = false;
}
};
////////////////////////////////////////////////////////////////////////////////
//// Asynchronous generator-based support subroutines
//// Asynchronous support subroutines
//
// The following functions are all generators that can be used inside the main
// test generator to perform specific tasks asynchronously. To invoke these
// subroutines correctly, an iteration syntax should be used:
//
// for (let yy in gen_example("Parameter")) yield undefined;
//
function gen_resetState(aData)
function promiseFocus()
{
let statement = Services.downloads.DBConnection.createAsyncStatement(
"DELETE FROM moz_downloads");
try {
statement.executeAsync({
handleResult: function(aResultSet) { },
handleError: function(aError)
{
Cu.reportError(aError);
},
handleCompletion: function(aReason)
{
testRunner.continueTest();
}
});
yield undefined;
} finally {
statement.finalize();
let deferred = Promise.defer();
waitForFocus(deferred.resolve);
return deferred.promise;
}
function promisePanelOpened()
{
let deferred = Promise.defer();
// Hook to wait until the panel is shown.
let originalOnPopupShown = DownloadsPanel.onPopupShown;
DownloadsPanel.onPopupShown = function () {
DownloadsPanel.onPopupShown = originalOnPopupShown;
originalOnPopupShown.apply(this, arguments);
// Defer to the next tick of the event loop so that we don't continue
// processing during the DOM event handler itself.
setTimeout(deferred.resolve, 0);
};
return deferred.promise;
}
function task_resetState()
{
// Remove all downloads.
let publicList = yield Downloads.getPublicDownloadList();
let downloads = yield publicList.getAll();
for (let download of downloads) {
publicList.remove(download);
yield download.finalize(true);
}
// Reset any prefs that might have been changed.
Services.prefs.clearUserPref("browser.download.panel.shown");
// Ensure that the panel is closed and data is unloaded.
aData.clear();
aData._loadState = aData.kLoadNone;
DownloadsPanel.hidePanel();
// Wait for focus on the main window.
waitForFocus(testRunner.continueTest);
yield undefined;
yield promiseFocus();
}
function gen_addDownloadRows(aDataRows)
function task_addDownloads(aItems)
{
let columnNames = Object.keys(gDownloadRowTemplate).join(", ");
let parameterNames = Object.keys(gDownloadRowTemplate)
.map(function(n) ":" + n)
.join(", ");
let statement = Services.downloads.DBConnection.createAsyncStatement(
"INSERT INTO moz_downloads (" + columnNames +
", guid) VALUES(" + parameterNames + ", GENERATE_GUID())");
try {
// Execute the statement for each of the provided downloads in reverse.
for (let i = aDataRows.length - 1; i >= 0; i--) {
let dataRow = aDataRows[i];
let startTimeMs = Date.now();
// Populate insert parameters from the provided data.
for (let columnName in gDownloadRowTemplate) {
if (!(columnName in dataRow)) {
// Update the provided row object with data from the global template,
// for columns whose value is not provided explicitly.
dataRow[columnName] = gDownloadRowTemplate[columnName];
}
statement.params[columnName] = dataRow[columnName];
}
// Run the statement asynchronously and wait.
statement.executeAsync({
handleResult: function(aResultSet) { },
handleError: function(aError)
{
Cu.reportError(aError.message + " (Result = " + aError.result + ")");
},
handleCompletion: function(aReason)
{
testRunner.continueTest();
}
});
yield undefined;
// At each iteration, ensure that the start and end time in the global
// template is distinct, as these column are used to sort each download
// in its category.
gDownloadRowTemplate.startTime++;
gDownloadRowTemplate.endTime++;
}
} finally {
statement.finalize();
let publicList = yield Downloads.getPublicDownloadList();
for (let item of aItems) {
publicList.add(yield Downloads.createDownload({
source: "http://www.example.com/test-download.txt",
target: gTestTargetFile,
succeeded: item.state == nsIDM.DOWNLOAD_FINISHED,
canceled: item.state == nsIDM.DOWNLOAD_CANCELED ||
item.state == nsIDM.DOWNLOAD_PAUSED,
error: item.state == nsIDM.DOWNLOAD_FAILED ? new Error("Failed.") : null,
hasPartialData: item.state == nsIDM.DOWNLOAD_PAUSED,
startTime: new Date(startTimeMs++),
}));
}
}
function gen_openPanel(aData)
function task_openPanel()
{
// Hook to wait until the test data has been loaded.
let originalOnViewLoadCompleted = DownloadsPanel.onViewLoadCompleted;
DownloadsPanel.onViewLoadCompleted = function () {
DownloadsPanel.onViewLoadCompleted = originalOnViewLoadCompleted;
originalOnViewLoadCompleted.apply(this);
testRunner.continueTest();
};
yield promiseFocus();
// Start loading all the downloads from the database asynchronously.
aData.ensurePersistentDataLoaded(false);
// Wait for focus on the main window.
waitForFocus(testRunner.continueTest);
yield undefined;
// Open the downloads panel, waiting until loading is completed.
let promise = promisePanelOpened();
DownloadsPanel.showPanel();
yield undefined;
}
/**
* Spin the event loop for aSeconds seconds, and then signal the test to
* continue.
*
* @param aSeconds the number of seconds to wait.
* @note This helper should _only_ be used when there's no valid event to
* listen to and one can't be made.
*/
function waitFor(aSeconds)
{
setTimeout(function() {
testRunner.continueTest();
}, aSeconds * 1000);
}
/**
* Make it so that the next time the downloads panel opens, we signal to
* continue the test. This function needs to be called each time you want
* to wait for the panel to open.
*
* Example usage:
*
* prepareForPanelOpen();
* // Do something to open the panel
* yield undefined;
* // We can assume the panel is open now.
*/
function prepareForPanelOpen()
{
// Hook to wait until the test data has been loaded.
let originalOnPopupShown = DownloadsPanel.onPopupShown;
DownloadsPanel.onPopupShown = function (aEvent) {
DownloadsPanel.onPopupShown = originalOnPopupShown;
DownloadsPanel.onPopupShown.apply(this, [aEvent]);
testRunner.continueTest();
};
yield promise;
}

View File

@ -262,6 +262,12 @@ let SessionSaverInternal = {
return;
}
// We update the time stamp before writing so that we don't write again
// too soon, if saving is requested before the write completes. Without
// this update we may save repeatedly if actions cause a runDelayed
// before writing has completed. See Bug 902280
this.updateLastSaveTime();
// Write (atomically) to a session file, using a tmp file. Once the session
// file is successfully updated, save the time stamp of the last save and
// notify the observers.

View File

@ -75,7 +75,7 @@ this._SessionFile = {
* state. This must only be called once, it will throw an error otherwise.
*/
writeLoadStateOnceAfterStartup: function (aLoadState) {
return SessionFileInternal.writeLoadStateOnceAfterStartup(aLoadState);
SessionFileInternal.writeLoadStateOnceAfterStartup(aLoadState);
},
/**
* Create a backup copy, asynchronously.
@ -95,7 +95,7 @@ this._SessionFile = {
* Wipe the contents of the session file, asynchronously.
*/
wipe: function () {
return SessionFileInternal.wipe();
SessionFileInternal.wipe();
}
};
@ -231,7 +231,7 @@ let SessionFileInternal = {
},
writeLoadStateOnceAfterStartup: function (aLoadState) {
return SessionWorker.post("writeLoadStateOnceAfterStartup", [aLoadState]).then(msg => {
SessionWorker.post("writeLoadStateOnceAfterStartup", [aLoadState]).then(msg => {
this._recordTelemetry(msg.telemetry);
return msg;
});
@ -246,7 +246,7 @@ let SessionFileInternal = {
},
wipe: function () {
return SessionWorker.post("wipe");
SessionWorker.post("wipe");
},
_recordTelemetry: function(telemetry) {

View File

@ -91,6 +91,11 @@ this.Social = {
providers: [],
_disabledForSafeMode: false,
get allowMultipleWorkers() {
return Services.prefs.prefHasUserValue("social.allowMultipleWorkers") &&
Services.prefs.getBoolPref("social.allowMultipleWorkers");
},
get _currentProviderPref() {
try {
return Services.prefs.getComplexValue("social.provider.current",
@ -114,15 +119,14 @@ this.Social = {
this._setProvider(val);
},
// Sets the current provider and enables it. Also disables the
// previously set provider, and notifies observers of the change.
// Sets the current provider and notifies observers of the change.
_setProvider: function (provider) {
if (this._provider == provider)
return;
// Disable the previous provider, if any, since we want only one provider to
// be enabled at once.
if (this._provider)
// Disable the previous provider, if we are not allowing multiple workers,
// since we want only one provider to be enabled at once.
if (this._provider && !Social.allowMultipleWorkers)
this._provider.enabled = false;
this._provider = provider;
@ -134,7 +138,6 @@ this.Social = {
let enabled = !!provider;
if (enabled != SocialService.enabled) {
SocialService.enabled = enabled;
Services.prefs.setBoolPref("social.enabled", enabled);
}
let origin = this._provider && this._provider.origin;
@ -159,31 +162,40 @@ this.Social = {
if (SocialService.enabled) {
// Retrieve the current set of providers, and set the current provider.
SocialService.getOrderedProviderList(function (providers) {
this._updateProviderCache(providers);
}.bind(this));
Social._updateProviderCache(providers);
Social._updateWorkerState(true);
});
}
// Register an observer for changes to the provider list
SocialService.registerProviderListener(function providerListener(topic, data) {
// An engine change caused by adding/removing a provider should notify
// An engine change caused by adding/removing a provider should notify.
// any providers we receive are enabled in the AddonsManager
if (topic == "provider-added" || topic == "provider-removed") {
this._updateProviderCache(data);
Social._updateProviderCache(data);
Social._updateWorkerState(true);
Services.obs.notifyObservers(null, "social:providers-changed", null);
return;
}
if (topic == "provider-update") {
// a provider has self-updated its manifest, we need to update our
// cache and possibly reload if it was the current provider.
// a provider has self-updated its manifest, we need to update our cache
// and reload the provider.
let provider = data;
// if we need a reload, do it now
if (provider.enabled) {
Social.enabled = false;
Services.tm.mainThread.dispatch(function() {
Social.enabled = true;
}, Components.interfaces.nsIThread.DISPATCH_NORMAL);
}
SocialService.getOrderedProviderList(function(providers) {
Social._updateProviderCache(providers);
provider.reload();
Services.obs.notifyObservers(null, "social:providers-changed", null);
});
}
}.bind(this));
});
},
_updateWorkerState: function(enable) {
// ensure that our providers are all disabled, and enabled if we allow
// multiple workers
if (enable && !Social.allowMultipleWorkers)
return;
[p.enabled = enable for (p of Social.providers) if (p.enabled != enable)];
},
// Called to update our cache of providers and set the current provider
@ -203,6 +215,9 @@ this.Social = {
set enabled(val) {
// Setting .enabled is just a shortcut for setting the provider to either
// the default provider or null...
this._updateWorkerState(val);
if (val) {
if (!this.provider)
this.provider = this.defaultProvider;
@ -210,6 +225,7 @@ this.Social = {
this.provider = null;
}
},
get enabled() {
return this.provider != null;
},
@ -229,10 +245,6 @@ this.Social = {
Services.prefs.setBoolPref("social.toast-notifications.enabled", !prefValue);
},
haveLoggedInUser: function () {
return !!(this.provider && this.provider.profile && this.provider.profile.userName);
},
setProviderByOrigin: function (origin) {
this.provider = this._getProviderFromOrigin(origin);
},

View File

@ -711,6 +711,7 @@ GK_ATOM(onerror, "onerror")
GK_ATOM(onfailed, "onfailed")
GK_ATOM(onfocus, "onfocus")
GK_ATOM(onget, "onget")
GK_ATOM(ongroupchange, "ongroupchange")
GK_ATOM(onhashchange, "onhashchange")
GK_ATOM(onheadphoneschange, "onheadphoneschange")
GK_ATOM(onheld, "onheld")

View File

@ -1162,6 +1162,11 @@ DOMInterfaces = {
'headerFile': 'TelephonyCall.h',
},
'TelephonyCallGroup' : {
'nativeType': 'mozilla::dom::telephony::TelephonyCallGroup',
'headerFile': 'TelephonyCallGroup.h',
},
'Text': {
# Total hack to allow binding code to realize that nsTextNode can
# in fact be cast to Text.

View File

@ -32,7 +32,8 @@ TelephonyListener::CallStateChanged(uint32_t aCallIndex,
const nsAString& aNumber,
bool aIsActive,
bool aIsOutgoing,
bool aIsEmergency)
bool aIsEmergency,
bool aIsConference)
{
BluetoothHfpManager* hfp = BluetoothHfpManager::Get();
hfp->HandleCallStateChanged(aCallIndex, aCallState, EmptyString(), aNumber,
@ -41,6 +42,12 @@ TelephonyListener::CallStateChanged(uint32_t aCallIndex,
return NS_OK;
}
NS_IMETHODIMP
TelephonyListener::ConferenceCallStateChanged(uint16_t aCallState)
{
return NS_OK;
}
NS_IMETHODIMP
TelephonyListener::EnumerateCallStateComplete()
{
@ -54,6 +61,7 @@ TelephonyListener::EnumerateCallState(uint32_t aCallIndex,
bool aIsActive,
bool aIsOutgoing,
bool aIsEmergency,
bool aIsConference,
bool* aResult)
{
BluetoothHfpManager* hfp = BluetoothHfpManager::Get();

View File

@ -108,7 +108,8 @@ const RIL_IPC_MSG_NAMES = [
"RIL:CdmaCallWaiting",
"RIL:ExitEmergencyCbMode",
"RIL:SetVoicePrivacyMode",
"RIL:GetVoicePrivacyMode"
"RIL:GetVoicePrivacyMode",
"RIL:ConferenceCallStateChanged"
];
XPCOMUtils.defineLazyServiceGetter(this, "cpmm",
@ -1392,6 +1393,31 @@ RILContentHelper.prototype = {
});
},
conferenceCall: function conferenceCall() {
cpmm.sendAsyncMessage("RIL:ConferenceCall", {
clientId: 0
});
},
separateCall: function separateCall(callIndex) {
cpmm.sendAsyncMessage("RIL:SeparateCall", {
clientId: 0,
data: callIndex
});
},
holdConference: function holdConference() {
cpmm.sendAsyncMessage("RIL:HoldConference", {
clientId: 0
});
},
resumeConference: function resumeConference() {
cpmm.sendAsyncMessage("RIL:ResumeConference", {
clientId: 0
});
},
get microphoneMuted() {
return cpmm.sendSyncMessage("RIL:GetMicrophoneMuted", {clientId: 0})[0];
},
@ -1528,7 +1554,15 @@ RILContentHelper.prototype = {
"callStateChanged",
[data.callIndex, data.state,
data.number, data.isActive,
data.isOutgoing, data.isEmergency]);
data.isOutgoing, data.isEmergency,
data.isConference]);
break;
}
case "RIL:ConferenceCallStateChanged": {
let data = msg.json.data;
this._deliverEvent("_telephonyListeners",
"conferenceCallStateChanged",
[data]);
break;
}
case "RIL:CallError": {
@ -1699,7 +1733,7 @@ RILContentHelper.prototype = {
keepGoing =
callback.enumerateCallState(call.callIndex, call.state, call.number,
call.isActive, call.isOutgoing,
call.isEmergency);
call.isEmergency, call.isConference);
} catch (e) {
debug("callback handler for 'enumerateCallState' threw an " +
" exception: " + e);

View File

@ -90,7 +90,11 @@ const RIL_IPC_TELEPHONY_MSG_NAMES = [
"RIL:RejectCall",
"RIL:HoldCall",
"RIL:ResumeCall",
"RIL:RegisterTelephonyMsg"
"RIL:RegisterTelephonyMsg",
"RIL:ConferenceCall",
"RIL:SeparateCall",
"RIL:HoldConference",
"RIL:ResumeConference"
];
const RIL_IPC_MOBILECONNECTION_MSG_NAMES = [
@ -930,6 +934,19 @@ RadioInterface.prototype = {
case "RIL:ResumeCall":
this.workerMessenger.send("resumeCall", { callIndex: msg.json.data });
break;
case "RIL:ConferenceCall":
this.workerMessenger.send("conferenceCall");
break;
case "RIL:SeparateCall":
this.workerMessenger.send("separateCall",
{ callIndex: msg.json.data });
break;
case "RIL:HoldConference":
this.workerMessenger.send("holdConference");
break;
case "RIL:ResumeConference":
this.workerMessenger.send("resumeConference");
break;
case "RIL:GetAvailableNetworks":
this.workerMessenger.sendWithIPCMessage(msg, "getAvailableNetworks");
break;
@ -1046,6 +1063,9 @@ RadioInterface.prototype = {
// This one will handle its own notifications.
this.handleCallDisconnected(message.call);
break;
case "conferenceCallStateChanged":
this.handleConferenceCallStateChanged(message.state);
break;
case "cdmaCallWaiting":
gMessageManager.sendTelephonyMessage("RIL:CdmaCallWaiting",
this.clientId, message.number);
@ -1657,7 +1677,35 @@ RadioInterface.prototype = {
* Track the active call and update the audio system as its state changes.
*/
_activeCall: null,
updateCallAudioState: function updateCallAudioState(call) {
updateCallAudioState: function updateCallAudioState(options) {
if (options.conferenceState === nsITelephonyProvider.CALL_STATE_CONNECTED) {
gAudioManager.phoneState = nsIAudioManager.PHONE_STATE_IN_CALL;
if (this.speakerEnabled) {
gAudioManager.setForceForUse(nsIAudioManager.USE_COMMUNICATION,
nsIAudioManager.FORCE_SPEAKER);
}
return;
}
if (options.conferenceState === nsITelephonyProvider.CALL_STATE_UNKNOWN ||
options.conferenceState === nsITelephonyProvider.CALL_STATE_HELD) {
if (!this._activeCall) {
gAudioManager.phoneState = nsIAudioManager.PHONE_STATE_NORMAL;
}
return;
}
if (!options.call) {
return;
}
if (options.call.isConference) {
if (this._activeCall && this._activeCall.callIndex == options.call.callIndex) {
this._activeCall = null;
}
return;
}
let call = options.call;
switch (call.state) {
case nsITelephonyProvider.CALL_STATE_DIALING: // Fall through...
case nsITelephonyProvider.CALL_STATE_ALERTING:
@ -1751,7 +1799,7 @@ RadioInterface.prototype = {
if (call.state == nsITelephonyProvider.CALL_STATE_DIALING) {
gSystemMessenger.broadcastMessage("telephony-new-call", {});
}
this.updateCallAudioState(call);
this.updateCallAudioState({call: call});
gMessageManager.sendTelephonyMessage("RIL:CallStateChanged",
this.clientId, call);
},
@ -1770,11 +1818,20 @@ RadioInterface.prototype = {
direction: call.isOutgoing ? "outgoing" : "incoming"
};
gSystemMessenger.broadcastMessage("telephony-call-ended", data);
this.updateCallAudioState(call);
this.updateCallAudioState({call: call});
gMessageManager.sendTelephonyMessage("RIL:CallStateChanged",
this.clientId, call);
},
handleConferenceCallStateChanged: function handleConferenceCallStateChanged(state) {
debug("handleConferenceCallStateChanged: " + state);
state = state != null ? convertRILCallState(state) :
nsITelephonyProvider.CALL_STATE_UNKNOWN;
this.updateCallAudioState({conferenceState: state});
gMessageManager.sendTelephonyMessage("RIL:ConferenceCallStateChanged",
this.clientId, state);
},
/**
* Update network selection mode
*/

View File

@ -735,6 +735,11 @@ let RIL = {
*/
currentCalls: {},
/**
* Existing conference call and its participants.
*/
currentConference: {state: null, participants: {}},
/**
* Existing data calls.
*/
@ -2069,6 +2074,29 @@ let RIL = {
}
},
// Flag indicating whether user has requested making a conference call.
_hasConferenceRequest: false,
conferenceCall: function conferenceCall(options) {
this._hasConferenceRequest = true;
Buf.simpleRequest(REQUEST_CONFERENCE, options);
},
separateCall: function separateCall(options) {
Buf.newParcel(REQUEST_SEPARATE_CONNECTION, options);
Buf.writeUint32(1);
Buf.writeUint32(options.callIndex);
Buf.sendParcel();
},
holdConference: function holdConference() {
Buf.simpleRequest(REQUEST_SWITCH_HOLDING_AND_ACTIVE);
},
resumeConference: function resumeConference() {
Buf.simpleRequest(REQUEST_SWITCH_HOLDING_AND_ACTIVE);
},
/**
* Send an SMS.
*
@ -3881,6 +3909,9 @@ let RIL = {
* Helpers for processing call state and handle the active call.
*/
_processCalls: function _processCalls(newCalls) {
let conferenceChanged = false;
let clearConferenceRequest = false;
// Go through the calls we currently have on file and see if any of them
// changed state. Remove them from the newCalls map as we deal with them
// so that only new calls remain in the map after we're done.
@ -3894,13 +3925,26 @@ let RIL = {
if (!newCall) {
// Call is no longer reported by the radio. Remove from our map and
// send disconnected state change.
delete this.currentCalls[currentCall.callIndex];
this.getFailCauseCode(currentCall);
if (this.currentConference.participants[currentCall.callIndex]) {
conferenceChanged = true;
currentCall.isConference = false;
delete this.currentConference.participants[currentCall.callIndex];
delete this.currentCalls[currentCall.callIndex];
// We don't query the fail cause here as it triggers another asynchrouns
// request that leads to a problem of updating all conferece participants
// in one task.
this._handleDisconnectedCall(currentCall);
} else {
delete this.currentCalls[currentCall.callIndex];
this.getFailCauseCode(currentCall);
}
continue;
}
// Call is still valid.
if (newCall.state == currentCall.state) {
if (newCall.state == currentCall.state &&
newCall.isMpty == currentCall.isMpty) {
continue;
}
@ -3916,8 +3960,71 @@ let RIL = {
if (!currentCall.started && newCall.state == CALL_STATE_ACTIVE) {
currentCall.started = new Date().getTime();
}
currentCall.state = newCall.state;
this._handleChangedCallState(currentCall);
if (currentCall.isMpty == newCall.isMpty &&
newCall.state != currentCall.state) {
currentCall.state = newCall.state;
if (currentCall.isConference) {
conferenceChanged = true;
}
this._handleChangedCallState(currentCall);
continue;
}
// '.isMpty' becomes false when the conference call is put on hold.
// We need to introduce additional 'isConference' to correctly record the
// real conference status
// Update a possible conference participant when .isMpty changes.
if (!currentCall.isMpty && newCall.isMpty) {
if (this._hasConferenceRequest) {
conferenceChanged = true;
clearConferenceRequest = true;
currentCall.state = newCall.state;
currentCall.isMpty = newCall.isMpty;
currentCall.isConference = true;
this.currentConference.participants[currentCall.callIndex] = currentCall;
this._handleChangedCallState(currentCall);
} else if (currentCall.isConference) {
// The case happens when resuming a held conference call.
conferenceChanged = true;
currentCall.state = newCall.state;
currentCall.isMpty = newCall.isMpty;
this.currentConference.participants[currentCall.callIndex] = currentCall;
this._handleChangedCallState(currentCall);
} else {
// Weird. This sometimes happens when we switch two calls, but it is
// not a conference call.
currentCall.state = newCall.state;
this._handleChangedCallState(currentCall);
}
} else if (currentCall.isMpty && !newCall.isMpty) {
if (!this.currentConference.participants[newCall.callIndex]) {
continue;
}
// '.isMpty' of a conference participant is set to false by rild when
// the conference call is put on hold. We don't actually know if the call
// still attends the conference until updating all calls finishes. We
// cache it for further determination.
if (newCall.state != CALL_STATE_HOLDING) {
delete this.currentConference.participants[newCall.callIndex];
currentCall.state = newCall.state;
currentCall.isMpty = newCall.isMpty;
currentCall.isConference = false;
conferenceChanged = true;
this._handleChangedCallState(currentCall);
continue;
}
if (!this.currentConference.cache) {
this.currentConference.cache = {};
}
this.currentConference.cache[currentCall.callIndex] = newCall;
currentCall.state = newCall.state;
currentCall.isMpty = newCall.isMpty;
conferenceChanged = true;
}
}
// Go through any remaining calls that are new to us.
@ -3944,16 +4051,69 @@ let RIL = {
}
// Add to our map.
this.currentCalls[newCall.callIndex] = newCall;
if (newCall.isMpty) {
conferenceChanged = true;
newCall.isConference = true;
this.currentConference.participants[newCall.callIndex] = newCall;
} else {
newCall.isConference = false;
}
this._handleChangedCallState(newCall);
this.currentCalls[newCall.callIndex] = newCall;
}
}
if (clearConferenceRequest) {
this._hasConferenceRequest = false;
}
if (conferenceChanged) {
this._ensureConference();
}
// Update our mute status. If there is anything in our currentCalls map then
// we know it's a voice call and we should leave audio on.
this.muted = (Object.getOwnPropertyNames(this.currentCalls).length === 0);
},
_ensureConference: function _ensureConference() {
let oldState = this.currentConference.state;
let remaining = Object.keys(this.currentConference.participants);
if (remaining.length == 1) {
// Remove that if only does one remain in a conference call.
let call = this.currentCalls[remaining[0]];
call.isConference = false;
this._handleChangedCallState(call);
delete this.currentConference.participants[call.callIndex];
} else if (remaining.length > 1) {
for each (let call in this.currentConference.cache) {
call.isConference = true;
this.currentConference.participants[call.callIndex] = call;
this.currentCalls[call.callIndex] = call;
this._handleChangedCallState(call);
}
}
delete this.currentConference.cache;
// Update the conference call's state.
let state = null;
for each (let call in this.currentConference.participants) {
if (state && state != call.state) {
// Each participant should have the same state, otherwise something
// wrong happens.
state = null;
break;
}
state = call.state;
}
if (oldState != state) {
this.currentConference.state = state;
let message = {rilMessageType: "conferenceCallStateChanged",
state: state};
this.sendChromeMessage(message);
}
},
_handleChangedCallState: function _handleChangedCallState(changedCall) {
let message = {rilMessageType: "callStateChange",
call: changedCall};
@ -5276,7 +5436,12 @@ RIL[REQUEST_SWITCH_HOLDING_AND_ACTIVE] = function REQUEST_SWITCH_HOLDING_AND_ACT
// this.getCurrentCalls() helps update the call state actively.
this.getCurrentCalls();
};
RIL[REQUEST_CONFERENCE] = null;
RIL[REQUEST_CONFERENCE] = function REQUEST_CONFERENCE(length, options) {
if (options.rilRequestError) {
this._hasConferenceRequest = false;
return;
}
};
RIL[REQUEST_UDUB] = null;
RIL[REQUEST_LAST_CALL_FAIL_CAUSE] = function REQUEST_LAST_CALL_FAIL_CAUSE(length, options) {
let num = 0;

View File

@ -9,11 +9,14 @@
#include "Telephony.h"
#include "TelephonyCall.h"
#include "TelephonyCallGroup.h"
USING_TELEPHONY_NAMESPACE
using namespace mozilla::dom;
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_1(CallsList, mTelephony)
NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_2(CallsList,
mTelephony,
mGroup)
NS_IMPL_CYCLE_COLLECTING_ADDREF(CallsList)
NS_IMPL_CYCLE_COLLECTING_RELEASE(CallsList)
@ -23,8 +26,8 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CallsList)
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
CallsList::CallsList(Telephony* aTelephony)
: mTelephony(aTelephony)
CallsList::CallsList(Telephony* aTelephony, TelephonyCallGroup* aGroup)
: mTelephony(aTelephony), mGroup(aGroup)
{
MOZ_ASSERT(mTelephony);
@ -50,20 +53,27 @@ CallsList::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aScope)
already_AddRefed<TelephonyCall>
CallsList::Item(uint32_t aIndex) const
{
nsRefPtr<TelephonyCall> call = mTelephony->CallsArray().SafeElementAt(aIndex);
nsRefPtr<TelephonyCall> call;
call = mGroup ? mGroup->CallsArray().SafeElementAt(aIndex) :
mTelephony->CallsArray().SafeElementAt(aIndex);
return call.forget();
}
uint32_t
CallsList::Length() const
{
return mTelephony->CallsArray().Length();
return mGroup ? mGroup->CallsArray().Length() :
mTelephony->CallsArray().Length();
}
already_AddRefed<TelephonyCall>
CallsList::IndexedGetter(uint32_t aIndex, bool& aFound) const
{
nsRefPtr<TelephonyCall> call = mTelephony->CallsArray().SafeElementAt(aIndex);
nsRefPtr<TelephonyCall> call;
call = mGroup ? mGroup->CallsArray().SafeElementAt(aIndex) :
mTelephony->CallsArray().SafeElementAt(aIndex);
aFound = call ? true : false;
return call.forget();
}

View File

@ -17,12 +17,13 @@ class CallsList MOZ_FINAL : public nsISupports,
public nsWrapperCache
{
nsRefPtr<Telephony> mTelephony;
nsRefPtr<TelephonyCallGroup> mGroup;
public:
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(CallsList)
CallsList(Telephony* aTelephony);
CallsList(Telephony* aTelephony, TelephonyCallGroup* aGroup = nullptr);
nsPIDOMWindow*
GetParentObject() const;

View File

@ -11,6 +11,7 @@
#include "nsPIDOMWindow.h"
#include "nsIPermissionManager.h"
#include "mozilla/dom/UnionTypes.h"
#include "nsCharSeparatedTokenizer.h"
#include "nsContentUtils.h"
#include "nsCxPusher.h"
@ -21,6 +22,7 @@
#include "CallEvent.h"
#include "CallsList.h"
#include "TelephonyCall.h"
#include "TelephonyCallGroup.h"
#define NS_RILCONTENTHELPER_CONTRACTID "@mozilla.org/ril/content-helper;1"
@ -89,13 +91,7 @@ Telephony::Telephony()
Telephony::~Telephony()
{
if (mListener) {
mListener->Disconnect();
if (mProvider) {
mProvider->UnregisterTelephonyMsg(mListener);
}
}
Shutdown();
NS_ASSERTION(gTelephonyList, "This should never be null!");
NS_ASSERTION(gTelephonyList->Contains(this), "Should be in the list!");
@ -103,12 +99,26 @@ Telephony::~Telephony()
if (gTelephonyList->Length() == 1) {
delete gTelephonyList;
gTelephonyList = nullptr;
}
else {
} else {
gTelephonyList->RemoveElement(this);
}
}
void
Telephony::Shutdown()
{
if (mListener) {
mListener->Disconnect();
if (mProvider) {
mProvider->UnregisterTelephonyMsg(mListener);
mProvider = nullptr;
}
mListener = nullptr;
}
}
JSObject*
Telephony::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aScope)
{
@ -147,6 +157,7 @@ Telephony::Create(nsPIDOMWindow* aOwner, ErrorResult& aRv)
telephony->mProvider = ril;
telephony->mListener = new Listener(telephony);
telephony->mCallsList = new CallsList(telephony);
telephony->mGroup = TelephonyCallGroup::Create(telephony);
nsresult rv = ril->EnumerateCalls(telephony->mListener);
if (NS_FAILED(rv)) {
@ -186,17 +197,6 @@ Telephony::NoteDialedCallFromOtherInstance(const nsAString& aNumber)
nsresult
Telephony::NotifyCallsChanged(TelephonyCall* aCall)
{
if (aCall) {
if (aCall->CallState() == nsITelephonyProvider::CALL_STATE_DIALING ||
aCall->CallState() == nsITelephonyProvider::CALL_STATE_ALERTING ||
aCall->CallState() == nsITelephonyProvider::CALL_STATE_CONNECTED) {
NS_ASSERTION(!mActiveCall, "Already have an active call!");
mActiveCall = aCall;
} else if (mActiveCall && mActiveCall->CallIndex() == aCall->CallIndex()) {
mActiveCall = nullptr;
}
}
return DispatchCallEvent(NS_LITERAL_STRING("callschanged"), aCall);
}
@ -247,19 +247,81 @@ Telephony::DialInternal(bool isEmergency,
return call.forget();
}
void
Telephony::UpdateActiveCall(TelephonyCall* aCall, bool aIsAdding)
{
if (aIsAdding) {
if (aCall->CallState() == nsITelephonyProvider::CALL_STATE_DIALING ||
aCall->CallState() == nsITelephonyProvider::CALL_STATE_ALERTING ||
aCall->CallState() == nsITelephonyProvider::CALL_STATE_CONNECTED) {
NS_ASSERTION(!mActiveCall, "Already have an active call!");
mActiveCall = aCall;
}
} else if (mActiveCall && mActiveCall->CallIndex() == aCall->CallIndex()) {
mActiveCall = nullptr;
}
}
already_AddRefed<TelephonyCall>
Telephony::GetCall(uint32_t aCallIndex)
{
nsRefPtr<TelephonyCall> call;
for (uint32_t index = 0; index < mCalls.Length(); index++) {
nsRefPtr<TelephonyCall>& tempCall = mCalls[index];
if (tempCall->CallIndex() == aCallIndex) {
call = tempCall;
break;
}
}
return call.forget();
}
bool
Telephony::MoveCall(uint32_t aCallIndex, bool aIsConference)
{
nsRefPtr<TelephonyCall> call;
// Move a call to mGroup.
if (aIsConference) {
call = GetCall(aCallIndex);
if (call) {
RemoveCall(call);
mGroup->AddCall(call);
return true;
}
return false;
}
// Remove a call from mGroup.
call = mGroup->GetCall(aCallIndex);
if (call) {
mGroup->RemoveCall(call);
AddCall(call);
return true;
}
return false;
}
NS_IMPL_CYCLE_COLLECTION_CLASS(Telephony)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(Telephony,
nsDOMEventTargetHelper)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCalls)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCallsList)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mGroup)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(Telephony,
nsDOMEventTargetHelper)
tmp->Shutdown();
tmp->mActiveCall = nullptr;
NS_IMPL_CYCLE_COLLECTION_UNLINK(mCalls)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mCallsList)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mGroup)
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(Telephony)
@ -316,11 +378,16 @@ Telephony::SetSpeakerEnabled(bool aEnabled, ErrorResult& aRv)
aRv = mProvider->SetSpeakerEnabled(aEnabled);
}
already_AddRefed<TelephonyCall>
Telephony::GetActive() const
void
Telephony::GetActive(Nullable<TelephonyCallOrTelephonyCallGroupReturnValue>& aValue)
{
nsCOMPtr<TelephonyCall> activeCall = mActiveCall;
return activeCall.forget();
if (mActiveCall) {
aValue.SetValue().SetAsTelephonyCall() = mActiveCall;
} else if (mGroup->CallState() == nsITelephonyProvider::CALL_STATE_CONNECTED) {
aValue.SetValue().SetAsTelephonyCallGroup() = mGroup;
} else {
aValue.SetNull();
}
}
already_AddRefed<CallsList>
@ -330,6 +397,13 @@ Telephony::Calls() const
return list.forget();
}
already_AddRefed<TelephonyCallGroup>
Telephony::ConferenceGroup() const
{
nsRefPtr<TelephonyCallGroup> group = mGroup;
return group.forget();
}
void
Telephony::StartTone(const nsAString& aDTMFChar, ErrorResult& aRv)
{
@ -368,7 +442,8 @@ Telephony::EventListenerAdded(nsIAtom* aType)
NS_IMETHODIMP
Telephony::CallStateChanged(uint32_t aCallIndex, uint16_t aCallState,
const nsAString& aNumber, bool aIsActive,
bool aIsOutgoing, bool aIsEmergency)
bool aIsOutgoing, bool aIsEmergency,
bool aIsConference)
{
NS_ASSERTION(aCallIndex != kOutgoingPlaceholderCallIndex,
"This should never happen!");
@ -376,6 +451,57 @@ Telephony::CallStateChanged(uint32_t aCallIndex, uint16_t aCallState,
nsRefPtr<TelephonyCall> modifiedCall;
nsRefPtr<TelephonyCall> outgoingCall;
// Update calls array first then state of a call in mCalls.
if (aIsConference) {
// Add the call into mGroup if it hasn't been there, otherwise we simply
// update its call state. We don't fire the statechange event on a call in
// conference here. Instead, the event will be fired later in
// TelephonyCallGroup::ChangeState(). Thus the sequence of firing the
// statechange events is guaranteed: first on TelephonyCallGroup then on
// individual TelephonyCall objects.
modifiedCall = mGroup->GetCall(aCallIndex);
if (modifiedCall) {
modifiedCall->ChangeStateInternal(aCallState, false);
return NS_OK;
}
// The call becomes a conference call. Remove it from Telephony::mCalls and
// add it to mGroup.
modifiedCall = GetCall(aCallIndex);
if (modifiedCall) {
modifiedCall->ChangeStateInternal(aCallState, false);
mGroup->AddCall(modifiedCall);
RemoveCall(modifiedCall);
return NS_OK;
}
// Didn't find this call in mCalls or mGroup. Create a new call.
nsRefPtr<TelephonyCall> call =
TelephonyCall::Create(this, aNumber, aCallState, aCallIndex,
aIsEmergency, aIsConference);
NS_ASSERTION(call, "This should never fail!");
return NS_OK;
}
// Not a conference call. Remove the call from mGroup if it has been there.
modifiedCall = mGroup->GetCall(aCallIndex);
if (modifiedCall) {
if (aCallState != nsITelephonyProvider::CALL_STATE_DISCONNECTED) {
if (modifiedCall->CallState() != aCallState) {
modifiedCall->ChangeState(aCallState);
}
mGroup->RemoveCall(modifiedCall);
AddCall(modifiedCall);
} else {
modifiedCall->ChangeState(aCallState);
}
return NS_OK;
}
// Update calls in mCalls.
for (uint32_t index = 0; index < mCalls.Length(); index++) {
nsRefPtr<TelephonyCall>& tempCall = mCalls[index];
if (tempCall->CallIndex() == kOutgoingPlaceholderCallIndex) {
@ -441,6 +567,13 @@ Telephony::CallStateChanged(uint32_t aCallIndex, uint16_t aCallState,
return NS_OK;
}
NS_IMETHODIMP
Telephony::ConferenceCallStateChanged(uint16_t aCallState)
{
mGroup->ChangeState(aCallState);
return NS_OK;
}
NS_IMETHODIMP
Telephony::EnumerateCallStateComplete()
{
@ -458,23 +591,36 @@ NS_IMETHODIMP
Telephony::EnumerateCallState(uint32_t aCallIndex, uint16_t aCallState,
const nsAString& aNumber, bool aIsActive,
bool aIsOutgoing, bool aIsEmergency,
bool* aContinue)
bool aIsConference, bool* aContinue)
{
// Make sure we don't somehow add duplicates.
for (uint32_t index = 0; index < mCalls.Length(); index++) {
nsRefPtr<TelephonyCall>& tempCall = mCalls[index];
if (tempCall->CallIndex() == aCallIndex) {
// We have the call already. Skip it.
*aContinue = true;
return NS_OK;
}
nsRefPtr<TelephonyCall> call;
// We request calls enumeration in constructor, and the asynchronous result
// will be sent back through the callback function EnumerateCallState().
// However, it is likely to have call state changes, i.e. CallStateChanged()
// being called, before the enumeration result comes back. We'd make sure
// we don't somehow add duplicates due to the race condition.
call = aIsConference ? mGroup->GetCall(aCallIndex) : GetCall(aCallIndex);
if (call) {
// We have the call either in mCalls or in mGroup. Skip it.
*aContinue = true;
return NS_OK;
}
nsRefPtr<TelephonyCall> call =
TelephonyCall::Create(this, aNumber, aCallState, aCallIndex, aIsEmergency);
if (MoveCall(aCallIndex, aIsConference)) {
*aContinue = true;
return NS_OK;
}
// Didn't know anything about this call before now.
call = TelephonyCall::Create(this, aNumber, aCallState, aCallIndex,
aIsEmergency, aIsConference);
NS_ASSERTION(call, "This should never fail!");
NS_ASSERTION(mCalls.Contains(call), "Should have auto-added new call!");
NS_ASSERTION(aIsConference ? mGroup->CallsArray().Contains(call) :
mCalls.Contains(call),
"Should have auto-added new call!");
*aContinue = true;
return NS_OK;
@ -486,13 +632,7 @@ Telephony::SupplementaryServiceNotification(int32_t aCallIndex,
{
nsRefPtr<TelephonyCall> associatedCall;
if (!mCalls.IsEmpty() && aCallIndex != -1) {
for (uint32_t index = 0; index < mCalls.Length(); index++) {
nsRefPtr<TelephonyCall>& call = mCalls[index];
if (call->CallIndex() == uint32_t(aCallIndex)) {
associatedCall = call;
break;
}
}
associatedCall = GetCall(aCallIndex);
}
nsresult rv;
@ -526,13 +666,7 @@ Telephony::NotifyError(int32_t aCallIndex,
}
} else {
// The connection has been established. Get the failed call.
for (uint32_t index = 0; index < mCalls.Length(); index++) {
nsRefPtr<TelephonyCall>& call = mCalls[index];
if (call->CallIndex() == aCallIndex) {
callToNotify = call;
break;
}
}
callToNotify = GetCall(aCallIndex);
}
}

View File

@ -8,14 +8,21 @@
#define mozilla_dom_telephony_telephony_h__
#include "TelephonyCommon.h"
#include "nsITelephonyProvider.h"
// Need to include TelephonyCall.h because we have inline methods that
// assume they see the definition of TelephonyCall.
#include "TelephonyCall.h"
#include "nsITelephonyProvider.h"
class nsPIDOMWindow;
namespace mozilla {
namespace dom {
class TelephonyCallOrTelephonyCallGroupReturnValue;
}
}
BEGIN_TELEPHONY_NAMESPACE
class Telephony MOZ_FINAL : public nsDOMEventTargetHelper
@ -39,6 +46,8 @@ class Telephony MOZ_FINAL : public nsDOMEventTargetHelper
nsTArray<nsRefPtr<TelephonyCall> > mCalls;
nsRefPtr<CallsList> mCallsList;
nsRefPtr<TelephonyCallGroup> mGroup;
bool mEnumerated;
public:
@ -77,12 +86,15 @@ public:
void
SetSpeakerEnabled(bool aEnabled, ErrorResult& aRv);
already_AddRefed<TelephonyCall>
GetActive() const;
void
GetActive(Nullable<TelephonyCallOrTelephonyCallGroupReturnValue>& aValue);
already_AddRefed<CallsList>
Calls() const;
already_AddRefed<TelephonyCallGroup>
ConferenceGroup() const;
void
StartTone(const nsAString& aDTMF, ErrorResult& aRv);
@ -104,6 +116,7 @@ public:
{
NS_ASSERTION(!mCalls.Contains(aCall), "Already know about this one!");
mCalls.AppendElement(aCall);
UpdateActiveCall(aCall, true);
NotifyCallsChanged(aCall);
}
@ -112,6 +125,7 @@ public:
{
NS_ASSERTION(mCalls.Contains(aCall), "Didn't know about this one!");
mCalls.RemoveElement(aCall);
UpdateActiveCall(aCall, false);
NotifyCallsChanged(aCall);
}
@ -153,6 +167,18 @@ private:
void
EnqueueEnumerationAck();
void
UpdateActiveCall(TelephonyCall* aCall, bool aIsAdding);
already_AddRefed<TelephonyCall>
GetCall(uint32_t aCallIndex);
bool
MoveCall(uint32_t aCallIndex, bool aIsConference);
void
Shutdown();
};
END_TELEPHONY_NAMESPACE

View File

@ -11,6 +11,7 @@
#include "CallEvent.h"
#include "Telephony.h"
#include "TelephonyCallGroup.h"
USING_TELEPHONY_NAMESPACE
using namespace mozilla::dom;
@ -18,7 +19,8 @@ using namespace mozilla::dom;
// static
already_AddRefed<TelephonyCall>
TelephonyCall::Create(Telephony* aTelephony, const nsAString& aNumber,
uint16_t aCallState, uint32_t aCallIndex, bool aEmergency)
uint16_t aCallState, uint32_t aCallIndex,
bool aEmergency, bool aIsConference)
{
NS_ASSERTION(aTelephony, "Null pointer!");
NS_ASSERTION(!aNumber.IsEmpty(), "Empty number!");
@ -33,6 +35,7 @@ TelephonyCall::Create(Telephony* aTelephony, const nsAString& aNumber,
call->mCallIndex = aCallIndex;
call->mError = nullptr;
call->mEmergency = aEmergency;
call->mGroup = aIsConference ? aTelephony->ConferenceGroup() : nullptr;
call->ChangeStateInternal(aCallState, false);
@ -108,10 +111,18 @@ TelephonyCall::ChangeStateInternal(uint16_t aCallState, bool aFireEvents)
if (aCallState == nsITelephonyProvider::CALL_STATE_DISCONNECTED) {
NS_ASSERTION(mLive, "Should be live!");
mTelephony->RemoveCall(this);
if (mGroup) {
mGroup->RemoveCall(this);
} else {
mTelephony->RemoveCall(this);
}
mLive = false;
} else if (!mLive) {
mTelephony->AddCall(this);
if (mGroup) {
mGroup->AddCall(this);
} else {
mTelephony->AddCall(this);
}
mLive = true;
}
@ -160,10 +171,22 @@ TelephonyCall::NotifyError(const nsAString& aError)
}
}
NS_IMPL_CYCLE_COLLECTION_INHERITED_2(TelephonyCall,
void
TelephonyCall::ChangeGroup(TelephonyCallGroup* aGroup)
{
mGroup = aGroup;
nsresult rv = DispatchCallEvent(NS_LITERAL_STRING("groupchange"), this);
if (NS_FAILED(rv)) {
NS_WARNING("Failed to dispatch error event!");
}
}
NS_IMPL_CYCLE_COLLECTION_INHERITED_3(TelephonyCall,
nsDOMEventTargetHelper,
mTelephony,
mError);
mError,
mGroup);
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(TelephonyCall)
NS_INTERFACE_MAP_END_INHERITING(nsDOMEventTargetHelper)
@ -180,6 +203,13 @@ TelephonyCall::GetError() const
return error.forget();
}
already_AddRefed<TelephonyCallGroup>
TelephonyCall::GetGroup() const
{
nsRefPtr<TelephonyCallGroup> group = mGroup;
return group.forget();
}
void
TelephonyCall::Answer(ErrorResult& aRv)
{
@ -225,6 +255,11 @@ TelephonyCall::Hold(ErrorResult& aRv)
return;
}
if (mGroup) {
NS_WARNING("Hold a call in conference ignored!");
return;
}
nsresult rv = mTelephony->Provider()->HoldCall(mCallIndex);
if (NS_FAILED(rv)) {
aRv.Throw(rv);
@ -242,6 +277,11 @@ TelephonyCall::Resume(ErrorResult& aRv)
return;
}
if (mGroup) {
NS_WARNING("Resume a call in conference ignored!");
return;
}
nsresult rv = mTelephony->Provider()->ResumeCall(mCallIndex);
if (NS_FAILED(rv)) {
aRv.Throw(rv);

View File

@ -18,6 +18,7 @@ BEGIN_TELEPHONY_NAMESPACE
class TelephonyCall MOZ_FINAL : public nsDOMEventTargetHelper
{
nsRefPtr<Telephony> mTelephony;
nsRefPtr<TelephonyCallGroup> mGroup;
nsString mNumber;
nsString mSecondNumber;
@ -36,6 +37,8 @@ public:
NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(TelephonyCall,
nsDOMEventTargetHelper)
friend class Telephony;
nsPIDOMWindow*
GetParentObject() const
{
@ -74,6 +77,9 @@ public:
already_AddRefed<DOMError>
GetError() const;
already_AddRefed<TelephonyCallGroup>
GetGroup() const;
void
Answer(ErrorResult& aRv);
@ -97,11 +103,12 @@ public:
IMPL_EVENT_HANDLER(held)
IMPL_EVENT_HANDLER(resuming)
IMPL_EVENT_HANDLER(error)
IMPL_EVENT_HANDLER(groupchange)
static already_AddRefed<TelephonyCall>
Create(Telephony* aTelephony, const nsAString& aNumber, uint16_t aCallState,
uint32_t aCallIndex = kOutgoingPlaceholderCallIndex,
bool aEmergency = false);
bool aEmergency = false, bool aIsConference = false);
void
ChangeState(uint16_t aCallState)
@ -150,6 +157,9 @@ public:
void
NotifyError(const nsAString& aError);
void
ChangeGroup(TelephonyCallGroup* aGroup);
private:
TelephonyCall();

View File

@ -0,0 +1,283 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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/. */
#include "TelephonyCallGroup.h"
#include "mozilla/dom/TelephonyCallGroupBinding.h"
#include "CallEvent.h"
#include "CallsList.h"
#include "Telephony.h"
USING_TELEPHONY_NAMESPACE
using namespace mozilla::dom;
TelephonyCallGroup::TelephonyCallGroup()
: mCallState(nsITelephonyProvider::CALL_STATE_UNKNOWN)
{
SetIsDOMBinding();
}
TelephonyCallGroup::~TelephonyCallGroup()
{
}
// static
already_AddRefed<TelephonyCallGroup>
TelephonyCallGroup::Create(Telephony* aTelephony)
{
NS_ASSERTION(aTelephony, "Null telephony!");
nsRefPtr<TelephonyCallGroup> group = new TelephonyCallGroup();
group->BindToOwner(aTelephony->GetOwner());
group->mTelephony = aTelephony;
group->mCallsList = new CallsList(aTelephony, group);
return group.forget();
}
JSObject*
TelephonyCallGroup::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aScope)
{
return TelephonyCallGroupBinding::Wrap(aCx, aScope, this);
}
void
TelephonyCallGroup::AddCall(TelephonyCall* aCall)
{
NS_ASSERTION(!mCalls.Contains(aCall), "Already know about this one!");
mCalls.AppendElement(aCall);
aCall->ChangeGroup(this);
NotifyCallsChanged(aCall);
}
void
TelephonyCallGroup::RemoveCall(TelephonyCall* aCall)
{
NS_ASSERTION(mCalls.Contains(aCall), "Didn't know about this one!");
mCalls.RemoveElement(aCall);
aCall->ChangeGroup(nullptr);
NotifyCallsChanged(aCall);
}
void
TelephonyCallGroup::ChangeState(uint16_t aCallState)
{
if (mCallState == aCallState) {
return;
}
nsString stateString;
switch (aCallState) {
case nsITelephonyProvider::CALL_STATE_UNKNOWN:
break;
case nsITelephonyProvider::CALL_STATE_CONNECTED:
stateString.AssignLiteral("connected");
break;
case nsITelephonyProvider::CALL_STATE_HOLDING:
stateString.AssignLiteral("holding");
break;
case nsITelephonyProvider::CALL_STATE_HELD:
stateString.AssignLiteral("held");
break;
case nsITelephonyProvider::CALL_STATE_RESUMING:
stateString.AssignLiteral("resuming");
break;
default:
NS_NOTREACHED("Unknown state!");
}
mState = stateString;
mCallState = aCallState;
nsresult rv = DispatchCallEvent(NS_LITERAL_STRING("statechange"), nullptr);
if (NS_FAILED(rv)) {
NS_WARNING("Failed to dispatch specific event!");
}
if (!stateString.IsEmpty()) {
// This can change if the statechange handler called back here... Need to
// figure out something smarter.
if (mCallState == aCallState) {
rv = DispatchCallEvent(stateString, nullptr);
if (NS_FAILED(rv)) {
NS_WARNING("Failed to dispatch specific event!");
}
}
}
for (uint32_t index = 0; index < mCalls.Length(); index++) {
nsRefPtr<TelephonyCall> call = mCalls[index];
call->ChangeState(aCallState);
MOZ_ASSERT(call->CallState() == aCallState);
}
}
nsresult
TelephonyCallGroup::NotifyCallsChanged(TelephonyCall* aCall)
{
return DispatchCallEvent(NS_LITERAL_STRING("callschanged"), aCall);
}
nsresult
TelephonyCallGroup::DispatchCallEvent(const nsAString& aType,
TelephonyCall* aCall)
{
nsRefPtr<CallEvent> event = CallEvent::Create(this, aType, aCall, false, false);
return DispatchTrustedEvent(event);
}
bool
TelephonyCallGroup::CanConference(const TelephonyCall& aCall,
TelephonyCall* aSecondCall)
{
if (!aSecondCall) {
MOZ_ASSERT(!mCalls.IsEmpty());
return (mCallState == nsITelephonyProvider::CALL_STATE_CONNECTED &&
aCall.CallState() == nsITelephonyProvider::CALL_STATE_HELD) ||
(mCallState == nsITelephonyProvider::CALL_STATE_HELD &&
aCall.CallState() == nsITelephonyProvider::CALL_STATE_CONNECTED);
}
MOZ_ASSERT(mCallState == nsITelephonyProvider::CALL_STATE_UNKNOWN);
return (aCall.CallState() == nsITelephonyProvider::CALL_STATE_CONNECTED &&
aSecondCall->CallState() == nsITelephonyProvider::CALL_STATE_HELD) ||
(aCall.CallState() == nsITelephonyProvider::CALL_STATE_HELD &&
aSecondCall->CallState() == nsITelephonyProvider::CALL_STATE_CONNECTED);
}
already_AddRefed<TelephonyCall>
TelephonyCallGroup::GetCall(uint32_t aCallIndex)
{
nsRefPtr<TelephonyCall> call;
for (uint32_t index = 0; index < mCalls.Length(); index++) {
nsRefPtr<TelephonyCall>& tempCall = mCalls[index];
if (tempCall->CallIndex() == aCallIndex) {
call = tempCall;
break;
}
}
return call.forget();
}
NS_IMPL_CYCLE_COLLECTION_CLASS(TelephonyCallGroup)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(TelephonyCallGroup,
nsDOMEventTargetHelper)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCalls)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCallsList)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTelephony)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(TelephonyCallGroup,
nsDOMEventTargetHelper)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mCalls)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mCallsList)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mTelephony)
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(TelephonyCallGroup)
NS_INTERFACE_MAP_END_INHERITING(nsDOMEventTargetHelper)
NS_IMPL_ADDREF_INHERITED(TelephonyCallGroup, nsDOMEventTargetHelper)
NS_IMPL_RELEASE_INHERITED(TelephonyCallGroup, nsDOMEventTargetHelper)
// WebIDL
already_AddRefed<CallsList>
TelephonyCallGroup::Calls() const
{
nsRefPtr<CallsList> list = mCallsList;
return list.forget();
}
void
TelephonyCallGroup::Add(TelephonyCall& aCall,
ErrorResult& aRv)
{
if (!CanConference(aCall, nullptr)) {
aRv.Throw(NS_ERROR_NOT_AVAILABLE);
return;
}
aRv = mTelephony->Provider()->ConferenceCall();
}
void
TelephonyCallGroup::Add(TelephonyCall& aCall,
TelephonyCall& aSecondCall,
ErrorResult& aRv)
{
if (!CanConference(aCall, &aSecondCall)) {
aRv.Throw(NS_ERROR_NOT_AVAILABLE);
return;
}
aRv = mTelephony->Provider()->ConferenceCall();
}
void
TelephonyCallGroup::Remove(TelephonyCall& aCall, ErrorResult& aRv)
{
if (mCallState != nsITelephonyProvider::CALL_STATE_CONNECTED) {
NS_WARNING("Remove call from a non-connected call group. Ignore!");
return;
}
uint32_t callIndex = aCall.CallIndex();
bool hasCallToRemove = false;
for (uint32_t index = 0; index < mCalls.Length(); index++) {
nsRefPtr<TelephonyCall>& call = mCalls[index];
if (call->CallIndex() == callIndex) {
hasCallToRemove = true;
break;
}
}
if (hasCallToRemove) {
aRv = mTelephony->Provider()->SeparateCall(callIndex);
} else {
NS_WARNING("Didn't have this call. Ignore!");
}
}
void
TelephonyCallGroup::Hold(ErrorResult& aRv)
{
if (mCallState != nsITelephonyProvider::CALL_STATE_CONNECTED) {
NS_WARNING("Hold non-connected call ignored!");
return;
}
nsresult rv = mTelephony->Provider()->HoldConference();
if (NS_FAILED(rv)) {
aRv.Throw(rv);
return;
}
ChangeState(nsITelephonyProvider::CALL_STATE_HOLDING);
}
void
TelephonyCallGroup::Resume(ErrorResult& aRv)
{
if (mCallState != nsITelephonyProvider::CALL_STATE_HELD) {
NS_WARNING("Resume non-held call ignored!");
return;
}
nsresult rv = mTelephony->Provider()->ResumeConference();
if (NS_FAILED(rv)) {
aRv.Throw(rv);
return;
}
ChangeState(nsITelephonyProvider::CALL_STATE_RESUMING);
}

View File

@ -0,0 +1,116 @@
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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/. */
#ifndef mozilla_dom_telephony_telephonycallgroup_h__
#define mozilla_dom_telephony_telephonycallgroup_h__
#include "TelephonyCommon.h"
BEGIN_TELEPHONY_NAMESPACE
class TelephonyCallGroup MOZ_FINAL : public nsDOMEventTargetHelper
{
nsRefPtr<Telephony> mTelephony;
nsTArray<nsRefPtr<TelephonyCall> > mCalls;
nsRefPtr<CallsList> mCallsList;
nsString mState;
uint16_t mCallState;
public:
NS_DECL_ISUPPORTS_INHERITED
NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(TelephonyCallGroup,
nsDOMEventTargetHelper)
nsPIDOMWindow*
GetParentObject() const
{
return GetOwner();
}
// WrapperCache
virtual JSObject*
WrapObject(JSContext* aCx, JS::Handle<JSObject*> aScope) MOZ_OVERRIDE;
// WebIDL interface
already_AddRefed<CallsList>
Calls() const;
void
Add(TelephonyCall& aCall, ErrorResult& aRv);
void
Add(TelephonyCall& aCall, TelephonyCall& aSecondCall, ErrorResult& aRv);
void
Remove(TelephonyCall& aCall, ErrorResult& aRv);
void
Hold(ErrorResult& aRv);
void
Resume(ErrorResult& aRv);
void
GetState(nsString& aState) const
{
aState = mState;
}
IMPL_EVENT_HANDLER(statechange)
IMPL_EVENT_HANDLER(connected)
IMPL_EVENT_HANDLER(holding)
IMPL_EVENT_HANDLER(held)
IMPL_EVENT_HANDLER(resuming)
IMPL_EVENT_HANDLER(callschanged)
static already_AddRefed<TelephonyCallGroup>
Create(Telephony* aTelephony);
void
AddCall(TelephonyCall* aCall);
void
RemoveCall(TelephonyCall* aCall);
already_AddRefed<TelephonyCall>
GetCall(uint32_t aCallIndex);
const nsTArray<nsRefPtr<TelephonyCall> >&
CallsArray() const
{
return mCalls;
}
void
ChangeState(uint16_t aCallState);
uint16_t
CallState() const
{
return mCallState;
}
private:
TelephonyCallGroup();
~TelephonyCallGroup();
nsresult
NotifyCallsChanged(TelephonyCall* aCall);
nsresult
DispatchCallEvent(const nsAString& aType,
TelephonyCall* aCall);
bool CanConference(const TelephonyCall& aCall, TelephonyCall* aSecondCall);
};
END_TELEPHONY_NAMESPACE
#endif // mozilla_dom_telephony_telephonycallgroup_h__

View File

@ -33,6 +33,7 @@ enum {
class CallsList;
class Telephony;
class TelephonyCall;
class TelephonyCallGroup;
END_TELEPHONY_NAMESPACE

View File

@ -17,5 +17,6 @@ CPP_SOURCES += [
'CallsList.cpp',
'Telephony.cpp',
'TelephonyCall.cpp',
'TelephonyCallGroup.cpp',
]

View File

@ -4,7 +4,7 @@
#include "nsISupports.idl"
[scriptable, uuid(3fb573c3-6fc4-41d3-80c1-f0e60662691e)]
[scriptable, uuid(a5818719-e1b6-4fdc-8551-006055fa9996)]
interface nsITelephonyListener : nsISupports
{
/**
@ -22,13 +22,27 @@ interface nsITelephonyListener : nsISupports
* Indicates whether this call is outgoing or incoming.
* @param isEmergency
* Indicates whether this call is an emergency call.
* @param isConference
* Indicates whether this call is a conference call.
*/
void callStateChanged(in unsigned long callIndex,
in unsigned short callState,
in AString number,
in boolean isActive,
in boolean isOutgoing,
in boolean isEmergency);
in boolean isEmergency,
in boolean isConference);
/**
* Called when participants of a conference call have been updated, and the
* conference call state changes.
*
* @param callState
* Possible values are: nsITelephonyProvider::CALL_STATE_UNKNOWN,
* nsITelephonyProvider::CALL_STATE_HELD,
* nsITelephonyProvider::CALL_STATE_CONNECTED.
*/
void conferenceCallStateChanged(in unsigned short callState);
/**
* Called when enumeration asked by nsITelephonyProvider::enumerateCalls
@ -51,7 +65,8 @@ interface nsITelephonyListener : nsISupports
* Indicates whether this call is the active one.
* @param isOutgoing
* Indicates whether this call is outgoing or incoming.
*
* @param isConference
* Indicates whether this call is a conference call.
* @return true to continue enumeration or false to cancel.
*/
boolean enumerateCallState(in unsigned long callIndex,
@ -59,7 +74,8 @@ interface nsITelephonyListener : nsISupports
in AString number,
in boolean isActive,
in boolean isOutgoing,
in boolean isEmergency);
in boolean isEmergency,
in boolean isConference);
/**
* Notify when RIL receives supplementary service notification.
@ -96,7 +112,7 @@ interface nsITelephonyListener : nsISupports
* XPCOM component (in the content process) that provides the telephony
* information.
*/
[scriptable, uuid(eddb7ff6-29af-4973-83de-1159365c7d7d)]
[scriptable, uuid(45a2f856-4e07-499a-94c6-624f90c3345b)]
interface nsITelephonyProvider : nsISupports
{
const unsigned short CALL_STATE_UNKNOWN = 0;
@ -143,6 +159,11 @@ interface nsITelephonyProvider : nsISupports
void holdCall(in unsigned long callIndex);
void resumeCall(in unsigned long callIndex);
void conferenceCall();
void separateCall(in unsigned long callIndex);
void holdConference();
void resumeConference();
attribute bool microphoneMuted;
attribute bool speakerEnabled;
};

View File

@ -15,9 +15,11 @@ interface Telephony : EventTarget {
[Throws]
attribute boolean speakerEnabled;
readonly attribute TelephonyCall? active;
readonly attribute (TelephonyCall or TelephonyCallGroup)? active;
// A call is contained either in Telephony or in TelephonyCallGroup.
readonly attribute CallsList calls;
readonly attribute TelephonyCallGroup conferenceGroup;
[Throws]
void startTone(DOMString tone);

View File

@ -20,6 +20,8 @@ interface TelephonyCall : EventTarget {
readonly attribute DOMError? error;
readonly attribute TelephonyCallGroup? group;
[Throws]
void answer();
[Throws]
@ -51,4 +53,8 @@ interface TelephonyCall : EventTarget {
attribute EventHandler onresuming;
[SetterThrows]
attribute EventHandler onerror;
// Fired whenever the group attribute changes.
[SetterThrows]
attribute EventHandler ongroupchange;
};

View File

@ -0,0 +1,39 @@
/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
/* vim: set ts=2 et sw=2 tw=40: */
/* 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/. */
interface TelephonyCallGroup : EventTarget {
readonly attribute CallsList calls;
[Throws]
void add(TelephonyCall call);
[Throws]
void add(TelephonyCall call, TelephonyCall secondCall);
[Throws]
void remove(TelephonyCall call);
[Throws]
void hold();
[Throws]
void resume();
readonly attribute DOMString state;
[SetterThrows]
attribute EventHandler onstatechange;
[SetterThrows]
attribute EventHandler onconnected;
[SetterThrows]
attribute EventHandler onholding;
[SetterThrows]
attribute EventHandler onheld;
[SetterThrows]
attribute EventHandler onresuming;
[SetterThrows]
attribute EventHandler oncallschanged;
};

View File

@ -444,6 +444,7 @@ webidl_files += \
MozStkCommandEvent.webidl \
Telephony.webidl \
TelephonyCall.webidl \
TelephonyCallGroup.webidl \
$(NULL)
endif

View File

@ -206,9 +206,9 @@ public:
ViewID mScrollId;
// The scrollable bounds of a frame. This is determined by reflow.
// For the top-level |window|,
// { x = window.scrollX, y = window.scrollY, // could be 0, 0
// width = window.innerWidth, height = window.innerHeight }
// Ordinarily the x and y will be 0 and the width and height will be the
// size of the element being scrolled. However for RTL pages or elements
// the x value may be negative.
//
// This is relative to the document. It is in the same coordinate space as
// |mScrollOffset|, but a different coordinate space than |mViewport| and

View File

@ -87,7 +87,7 @@ APZCTreeManager::UpdatePanZoomControllerTree(CompositorParent* aCompositor, Laye
&apzcsToDestroy);
}
for (int i = apzcsToDestroy.Length() - 1; i >= 0; i--) {
for (size_t i = 0; i < apzcsToDestroy.Length(); i++) {
APZC_LOG("Destroying APZC at %p\n", apzcsToDestroy[i].get());
apzcsToDestroy[i]->Destroy();
}
@ -211,26 +211,41 @@ ApplyTransform(nsIntPoint* aPoint, const gfx3DMatrix& aMatrix)
nsEventStatus
APZCTreeManager::ReceiveInputEvent(const InputData& aEvent)
{
nsRefPtr<AsyncPanZoomController> apzc;
gfx3DMatrix transformToApzc;
gfx3DMatrix transformToScreen;
switch (aEvent.mInputType) {
case MULTITOUCH_INPUT: {
const MultiTouchInput& multiTouchInput = aEvent.AsMultiTouchInput();
apzc = GetTargetAPZC(ScreenPoint(multiTouchInput.mTouches[0].mScreenPoint),
transformToApzc, transformToScreen);
if (apzc) {
if (multiTouchInput.mType == MultiTouchInput::MULTITOUCH_START) {
mApzcForInputBlock = GetTargetAPZC(ScreenPoint(multiTouchInput.mTouches[0].mScreenPoint));
for (size_t i = 1; i < multiTouchInput.mTouches.Length(); i++) {
nsRefPtr<AsyncPanZoomController> apzc2 = GetTargetAPZC(ScreenPoint(multiTouchInput.mTouches[i].mScreenPoint));
mApzcForInputBlock = CommonAncestor(mApzcForInputBlock.get(), apzc2.get());
APZC_LOG("Using APZC %p as the common ancestor\n", mApzcForInputBlock.get());
}
} else if (mApzcForInputBlock) {
APZC_LOG("Re-using APZC %p as continuation of event block\n", mApzcForInputBlock.get());
// If we have an mApzcForInputBlock and it's the end of the touch sequence
// then null it out so we don't keep a dangling reference and leak things.
if (multiTouchInput.mType == MultiTouchInput::MULTITOUCH_CANCEL ||
(multiTouchInput.mType == MultiTouchInput::MULTITOUCH_END && multiTouchInput.mTouches.Length() == 1)) {
mApzcForInputBlock = nullptr;
}
}
if (mApzcForInputBlock) {
GetInputTransforms(mApzcForInputBlock, transformToApzc, transformToScreen);
MultiTouchInput inputForApzc(multiTouchInput);
for (int i = inputForApzc.mTouches.Length() - 1; i >= 0; i--) {
for (size_t i = 0; i < inputForApzc.mTouches.Length(); i++) {
ApplyTransform(&(inputForApzc.mTouches[i].mScreenPoint), transformToApzc);
}
apzc->ReceiveInputEvent(inputForApzc);
mApzcForInputBlock->ReceiveInputEvent(inputForApzc);
}
break;
} case PINCHGESTURE_INPUT: {
const PinchGestureInput& pinchInput = aEvent.AsPinchGestureInput();
apzc = GetTargetAPZC(pinchInput.mFocusPoint, transformToApzc, transformToScreen);
nsRefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(pinchInput.mFocusPoint);
if (apzc) {
GetInputTransforms(apzc, transformToApzc, transformToScreen);
PinchGestureInput inputForApzc(pinchInput);
ApplyTransform(&(inputForApzc.mFocusPoint), transformToApzc);
apzc->ReceiveInputEvent(inputForApzc);
@ -238,8 +253,9 @@ APZCTreeManager::ReceiveInputEvent(const InputData& aEvent)
break;
} case TAPGESTURE_INPUT: {
const TapGestureInput& tapInput = aEvent.AsTapGestureInput();
apzc = GetTargetAPZC(ScreenPoint(tapInput.mPoint), transformToApzc, transformToScreen);
nsRefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(ScreenPoint(tapInput.mPoint));
if (apzc) {
GetInputTransforms(apzc, transformToApzc, transformToScreen);
TapGestureInput inputForApzc(tapInput);
ApplyTransform(&(inputForApzc.mPoint), transformToApzc);
apzc->ReceiveInputEvent(inputForApzc);
@ -256,38 +272,54 @@ APZCTreeManager::ReceiveInputEvent(const nsInputEvent& aEvent,
{
MOZ_ASSERT(NS_IsMainThread());
nsRefPtr<AsyncPanZoomController> apzc;
gfx3DMatrix transformToApzc;
gfx3DMatrix transformToScreen;
switch (aEvent.eventStructType) {
case NS_TOUCH_EVENT: {
const nsTouchEvent& touchEvent = static_cast<const nsTouchEvent&>(aEvent);
if (touchEvent.touches.Length() > 0) {
if (touchEvent.touches.Length() == 0) {
break;
}
if (touchEvent.message == NS_TOUCH_START) {
nsIntPoint point = touchEvent.touches[0]->mRefPoint;
apzc = GetTargetAPZC(ScreenPoint::FromUnknownPoint(gfx::Point(point.x, point.y)),
transformToApzc, transformToScreen);
if (apzc) {
MultiTouchInput inputForApzc(touchEvent);
for (int i = inputForApzc.mTouches.Length() - 1; i >= 0; i--) {
ApplyTransform(&(inputForApzc.mTouches[i].mScreenPoint), transformToApzc);
}
gfx3DMatrix outTransform = transformToApzc * transformToScreen;
nsTouchEvent* outEvent = static_cast<nsTouchEvent*>(aOutEvent);
for (int i = outEvent->touches.Length() - 1; i >= 0; i--) {
ApplyTransform(&(outEvent->touches[i]->mRefPoint), outTransform);
}
return apzc->ReceiveInputEvent(inputForApzc);
mApzcForInputBlock = GetTargetAPZC(ScreenPoint(point.x, point.y));
for (size_t i = 1; i < touchEvent.touches.Length(); i++) {
point = touchEvent.touches[i]->mRefPoint;
nsRefPtr<AsyncPanZoomController> apzc2 =
GetTargetAPZC(ScreenPoint(point.x, point.y));
mApzcForInputBlock = CommonAncestor(mApzcForInputBlock.get(), apzc2.get());
APZC_LOG("Using APZC %p as the common ancestor\n", mApzcForInputBlock.get());
}
} else if (mApzcForInputBlock) {
APZC_LOG("Re-using APZC %p as continuation of event block\n", mApzcForInputBlock.get());
// If we have an mApzcForInputBlock and it's the end of the touch sequence
// then null it out so we don't keep a dangling reference and leak things.
if (touchEvent.message == NS_TOUCH_CANCEL ||
(touchEvent.message == NS_TOUCH_END && touchEvent.touches.Length() == 1)) {
mApzcForInputBlock = nullptr;
}
}
if (mApzcForInputBlock) {
GetInputTransforms(mApzcForInputBlock, transformToApzc, transformToScreen);
MultiTouchInput inputForApzc(touchEvent);
for (size_t i = 0; i < inputForApzc.mTouches.Length(); i++) {
ApplyTransform(&(inputForApzc.mTouches[i].mScreenPoint), transformToApzc);
}
gfx3DMatrix outTransform = transformToApzc * transformToScreen;
nsTouchEvent* outEvent = static_cast<nsTouchEvent*>(aOutEvent);
for (size_t i = 0; i < outEvent->touches.Length(); i++) {
ApplyTransform(&(outEvent->touches[i]->mRefPoint), outTransform);
}
return mApzcForInputBlock->ReceiveInputEvent(inputForApzc);
}
break;
} case NS_MOUSE_EVENT: {
const nsMouseEvent& mouseEvent = static_cast<const nsMouseEvent&>(aEvent);
apzc = GetTargetAPZC(ScreenPoint::FromUnknownPoint(gfx::Point(mouseEvent.refPoint.x,
mouseEvent.refPoint.y)),
transformToApzc, transformToScreen);
nsRefPtr<AsyncPanZoomController> apzc = GetTargetAPZC(ScreenPoint(mouseEvent.refPoint.x, mouseEvent.refPoint.y));
if (apzc) {
GetInputTransforms(apzc, transformToApzc, transformToScreen);
MultiTouchInput inputForApzc(mouseEvent);
ApplyTransform(&(inputForApzc.mTouches[0].mScreenPoint), transformToApzc);
@ -394,7 +426,7 @@ APZCTreeManager::ClearTree()
// If this is too slow feel free to change it to a recursive walk.
nsTArray< nsRefPtr<AsyncPanZoomController> > apzcsToDestroy;
Collect(mRootApzc, &apzcsToDestroy);
for (int i = apzcsToDestroy.Length() - 1; i >= 0; i--) {
for (size_t i = 0; i < apzcsToDestroy.Length(); i++) {
apzcsToDestroy[i]->Destroy();
}
mRootApzc = nullptr;
@ -415,11 +447,78 @@ APZCTreeManager::GetTargetAPZC(const ScrollableLayerGuid& aGuid)
return target.forget();
}
/* This function returns the AsyncPanZoomController instance that hit testing determines
is under the given ScreenPoint.
already_AddRefed<AsyncPanZoomController>
APZCTreeManager::GetTargetAPZC(const ScreenPoint& aPoint)
{
MonitorAutoLock lock(mTreeLock);
nsRefPtr<AsyncPanZoomController> target;
// The root may have siblings, so check those too
gfxPoint point(aPoint.x, aPoint.y);
for (AsyncPanZoomController* apzc = mRootApzc; apzc; apzc = apzc->GetPrevSibling()) {
target = GetAPZCAtPoint(apzc, point);
if (target) {
break;
}
}
return target.forget();
}
In addition, the aTransformToApzcOut and aTransformToScreenOut out-parameters are filled
with some useful transformations that input events may need applied. This is best
AsyncPanZoomController*
APZCTreeManager::FindTargetAPZC(AsyncPanZoomController* aApzc, const ScrollableLayerGuid& aGuid) {
// This walks the tree in depth-first, reverse order, so that it encounters
// APZCs front-to-back on the screen.
for (AsyncPanZoomController* child = aApzc->GetLastChild(); child; child = child->GetPrevSibling()) {
AsyncPanZoomController* match = FindTargetAPZC(child, aGuid);
if (match) {
return match;
}
}
if (aApzc->Matches(aGuid)) {
return aApzc;
}
return nullptr;
}
AsyncPanZoomController*
APZCTreeManager::GetAPZCAtPoint(AsyncPanZoomController* aApzc, const gfxPoint& aHitTestPoint)
{
// The comments below assume there is a chain of layers L..R with L and P having APZC instances as
// explained in the comment on GetInputTransforms. This function will recurse with aApzc at L and P, and the
// comments explain what values are stored in the variables at these two levels. All the comments
// use standard matrix notation where the leftmost matrix in a multiplication is applied first.
// ancestorUntransform is OC.Inverse() * NC.Inverse() * MC.Inverse() at recursion level for L,
// and RC.Inverse() * QC.Inverse() at recursion level for P.
gfx3DMatrix ancestorUntransform = aApzc->GetAncestorTransform().Inverse();
// asyncUntransform is LA.Inverse() at recursion level for L,
// and PA.Inverse() at recursion level for P.
gfx3DMatrix asyncUntransform = gfx3DMatrix(aApzc->GetCurrentAsyncTransform()).Inverse();
// untransformSinceLastApzc is OC.Inverse() * NC.Inverse() * MC.Inverse() * LA.Inverse() * LC.Inverse() at L,
// and RC.Inverse() * QC.Inverse() * PA.Inverse() * PC.Inverse() at P.
gfx3DMatrix untransformSinceLastApzc = ancestorUntransform * asyncUntransform * aApzc->GetCSSTransform().Inverse();
// untransformed is the user input in L's layer space at L,
// and in P's layer space at P.
gfxPoint untransformed = untransformSinceLastApzc.ProjectPoint(aHitTestPoint);
APZC_LOG("Untransformed %f %f to %f %f for APZC %p\n", aHitTestPoint.x, aHitTestPoint.y, untransformed.x, untransformed.y, aApzc);
// This walks the tree in depth-first, reverse order, so that it encounters
// APZCs front-to-back on the screen.
for (AsyncPanZoomController* child = aApzc->GetLastChild(); child; child = child->GetPrevSibling()) {
AsyncPanZoomController* match = GetAPZCAtPoint(child, untransformed);
if (match) {
return match;
}
}
if (aApzc->VisibleRegionContains(LayerPoint(untransformed.x, untransformed.y))) {
APZC_LOG("Successfully matched untransformed point %f %f to visible region for APZC %p\n", untransformed.x, untransformed.y, aApzc);
return aApzc;
}
return nullptr;
}
/* This function sets the aTransformToApzcOut and aTransformToScreenOut out-parameters
to some useful transformations that input events may need applied. This is best
illustrated with an example. Consider a chain of layers, L, M, N, O, P, Q, R. Layer L
is the layer that corresponds to the returned APZC instance, and layer R is the root
of the layer tree. Layer M is the parent of L, N is the parent of M, and so on.
@ -478,93 +577,84 @@ APZCTreeManager::GetTargetAPZC(const ScrollableLayerGuid& aGuid)
layer L would store LC and (MC * NC * OC), and the layer P would store PC and (QC * RC).
The APZCs also obviously have LA and PA, so all of the above transformation combinations
required can be generated.
Note that this function may return null, in which case the matrix out-parameters are
left unmodified.
*/
already_AddRefed<AsyncPanZoomController>
APZCTreeManager::GetTargetAPZC(const ScreenPoint& aPoint,
gfx3DMatrix& aTransformToApzcOut,
gfx3DMatrix& aTransformToScreenOut)
{
MonitorAutoLock lock(mTreeLock);
nsRefPtr<AsyncPanZoomController> target;
// The root may have siblings, so check those too
gfxPoint point(aPoint.x, aPoint.y);
for (AsyncPanZoomController* apzc = mRootApzc; apzc; apzc = apzc->GetPrevSibling()) {
target = GetAPZCAtPoint(apzc, point, aTransformToApzcOut, aTransformToScreenOut);
if (target) {
break;
}
}
return target.forget();
}
AsyncPanZoomController*
APZCTreeManager::FindTargetAPZC(AsyncPanZoomController* aApzc, const ScrollableLayerGuid& aGuid) {
// This walks the tree in depth-first, reverse order, so that it encounters
// APZCs front-to-back on the screen.
for (AsyncPanZoomController* child = aApzc->GetLastChild(); child; child = child->GetPrevSibling()) {
AsyncPanZoomController* match = FindTargetAPZC(child, aGuid);
if (match) {
return match;
}
}
if (aApzc->Matches(aGuid)) {
return aApzc;
}
return nullptr;
}
AsyncPanZoomController*
APZCTreeManager::GetAPZCAtPoint(AsyncPanZoomController* aApzc, const gfxPoint& aHitTestPoint,
gfx3DMatrix& aTransformToApzcOut, gfx3DMatrix& aTransformToScreenOut)
void
APZCTreeManager::GetInputTransforms(AsyncPanZoomController *aApzc, gfx3DMatrix& aTransformToApzcOut,
gfx3DMatrix& aTransformToScreenOut)
{
// The comments below assume there is a chain of layers L..R with L and P having APZC instances as
// explained in the comment on GetTargetAPZC. This function will recurse with aApzc at L and P, and the
// comments explain what values are stored in the variables at these two levels. All the comments
// use standard matrix notation where the leftmost matrix in a multiplication is applied first.
// explained in the comment above. This function is called with aApzc at L, and the loop
// below performs one iteration, where parent is at P. The comments explain what values are stored
// in the variables at these two levels. All the comments use standard matrix notation where the
// leftmost matrix in a multiplication is applied first.
// ancestorUntransform is OC.Inverse() * NC.Inverse() * MC.Inverse() at recursion level for L,
// and RC.Inverse() * QC.Inverse() at recursion level for P.
// ancestorUntransform is OC.Inverse() * NC.Inverse() * MC.Inverse()
gfx3DMatrix ancestorUntransform = aApzc->GetAncestorTransform().Inverse();
// asyncUntransform is LA.Inverse() at recursion level for L,
// and PA.Inverse() at recursion level for P.
// asyncUntransform is LA.Inverse()
gfx3DMatrix asyncUntransform = gfx3DMatrix(aApzc->GetCurrentAsyncTransform()).Inverse();
// untransformSinceLastApzc is OC.Inverse() * NC.Inverse() * MC.Inverse() * LA.Inverse() * LC.Inverse() at L,
// and RC.Inverse() * QC.Inverse() * PA.Inverse() * PC.Inverse() at P.
gfx3DMatrix untransformSinceLastApzc = ancestorUntransform * asyncUntransform * aApzc->GetCSSTransform().Inverse();
// untransformed is the user input in L's layer space at L,
// and in P's layer space at P.
gfxPoint untransformed = untransformSinceLastApzc.ProjectPoint(aHitTestPoint);
APZC_LOG("Untransformed %f %f to %f %f for APZC %p\n", aHitTestPoint.x, aHitTestPoint.y, untransformed.x, untransformed.y, aApzc);
// This walks the tree in depth-first, reverse order, so that it encounters
// APZCs front-to-back on the screen.
for (AsyncPanZoomController* child = aApzc->GetLastChild(); child; child = child->GetPrevSibling()) {
AsyncPanZoomController* match = GetAPZCAtPoint(child, untransformed, aTransformToApzcOut, aTransformToScreenOut);
if (match) {
// This code is not run in the recursion at layer L.
// aTransformToApzcOut is RC.Inverse() * QC.Inverse() * PA.Inverse() * PC.Inverse() * OC.Inverse() * NC.Inverse() * MC.Inverse()
// at recursion level for P
aTransformToApzcOut = untransformSinceLastApzc * aTransformToApzcOut;
// aTransformToScreenOut is LA.Inverse() * MC * NC * OC * PC * QC * RC at recursion level for P
aTransformToScreenOut = aTransformToScreenOut * aApzc->GetCSSTransform() * aApzc->GetAncestorTransform();
// The above values for aTransformToApzcOut and aTransformToScreenOut at recursion level for P match
// the required output as explained in the comment above GetTargetAPZC. Note that any missing terms
// are async transforms that are guaranteed to be identity transforms.
return match;
}
// aTransformToApzcOut is initialized to OC.Inverse() * NC.Inverse() * MC.Inverse()
aTransformToApzcOut = ancestorUntransform;
// aTransformToScreenOut is initialized to LA.Inverse() * MC * NC * OC
aTransformToScreenOut = asyncUntransform * aApzc->GetAncestorTransform();
for (AsyncPanZoomController* parent = aApzc->GetParent(); parent; parent = parent->GetParent()) {
// ancestorUntransform is updated to RC.Inverse() * QC.Inverse() when parent == P
ancestorUntransform = parent->GetAncestorTransform().Inverse();
// asyncUntransform is updated to PA.Inverse() when parent == P
asyncUntransform = gfx3DMatrix(parent->GetCurrentAsyncTransform()).Inverse();
// untransformSinceLastApzc is RC.Inverse() * QC.Inverse() * PA.Inverse() * PC.Inverse()
gfx3DMatrix untransformSinceLastApzc = ancestorUntransform * asyncUntransform * parent->GetCSSTransform().Inverse();
// aTransformToApzcOut is RC.Inverse() * QC.Inverse() * PA.Inverse() * PC.Inverse() * OC.Inverse() * NC.Inverse() * MC.Inverse()
aTransformToApzcOut = untransformSinceLastApzc * aTransformToApzcOut;
// aTransformToScreenOut is LA.Inverse() * MC * NC * OC * PC * QC * RC
aTransformToScreenOut = aTransformToScreenOut * parent->GetCSSTransform() * parent->GetAncestorTransform();
// The above values for aTransformToApzcOut and aTransformToScreenOut when parent == P match
// the required output as explained in the comment above GetTargetAPZC. Note that any missing terms
// are async transforms that are guaranteed to be identity transforms.
}
if (aApzc->VisibleRegionContains(LayerPoint(untransformed.x, untransformed.y))) {
APZC_LOG("Successfully matched untransformed point %f %f to visible region for APZC %p\n", untransformed.x, untransformed.y, aApzc);
// This code is not run in the recursion at layer P.
// aTransformToApzcOut is OC.Inverse() * NC.Inverse() * MC.Inverse() at recursion level for L.
aTransformToApzcOut = ancestorUntransform;
// aTransformToScreenOut is LA.Inverse() * MC * NC * OC at recursion level for L.
aTransformToScreenOut = asyncUntransform * aApzc->GetAncestorTransform();
return aApzc;
}
AsyncPanZoomController*
APZCTreeManager::CommonAncestor(AsyncPanZoomController* aApzc1, AsyncPanZoomController* aApzc2)
{
// If either aApzc1 or aApzc2 is null, min(depth1, depth2) will be 0 and this function
// will return null.
// Calculate depth of the APZCs in the tree
int depth1 = 0, depth2 = 0;
for (AsyncPanZoomController* parent = aApzc1; parent; parent = parent->GetParent()) {
depth1++;
}
for (AsyncPanZoomController* parent = aApzc2; parent; parent = parent->GetParent()) {
depth2++;
}
// At most one of the following two loops will be executed; the deeper APZC pointer
// will get walked up to the depth of the shallower one.
int minDepth = depth1 < depth2 ? depth1 : depth2;
while (depth1 > minDepth) {
depth1--;
aApzc1 = aApzc1->GetParent();
}
while (depth2 > minDepth) {
depth2--;
aApzc2 = aApzc2->GetParent();
}
// Walk up the ancestor chains of both APZCs, always staying at the same depth for
// either APZC, and return the the first common ancestor encountered.
while (true) {
if (aApzc1 == aApzc2) {
return aApzc1;
}
if (depth1 <= 0) {
break;
}
aApzc1 = aApzc1->GetParent();
aApzc2 = aApzc2->GetParent();
}
return nullptr;
}

View File

@ -142,7 +142,6 @@ public:
* General handler for incoming input events. Manipulates the frame metrics
* based on what type of input it is. For example, a PinchGestureEvent will
* cause scaling. This should only be called externally to this class.
* HandleInputEvent() should be used internally.
*/
nsEventStatus ReceiveInputEvent(const InputData& aEvent);
@ -251,13 +250,14 @@ public:
used by other production code.
*/
already_AddRefed<AsyncPanZoomController> GetTargetAPZC(const ScrollableLayerGuid& aGuid);
already_AddRefed<AsyncPanZoomController> GetTargetAPZC(const ScreenPoint& aPoint, gfx3DMatrix& aTransformToApzcOut,
gfx3DMatrix& aTransformToScreenOut);
already_AddRefed<AsyncPanZoomController> GetTargetAPZC(const ScreenPoint& aPoint);
void GetInputTransforms(AsyncPanZoomController *aApzc, gfx3DMatrix& aTransformToApzcOut,
gfx3DMatrix& aTransformToScreenOut);
private:
/* Recursive helpers */
/* Helpers */
AsyncPanZoomController* FindTargetAPZC(AsyncPanZoomController* aApzc, const ScrollableLayerGuid& aGuid);
AsyncPanZoomController* GetAPZCAtPoint(AsyncPanZoomController* aApzc, const gfxPoint& aHitTestPoint,
gfx3DMatrix& aTransformToApzcOut, gfx3DMatrix& aTransformToScreenOut);
AsyncPanZoomController* GetAPZCAtPoint(AsyncPanZoomController* aApzc, const gfxPoint& aHitTestPoint);
AsyncPanZoomController* CommonAncestor(AsyncPanZoomController* aApzc1, AsyncPanZoomController* aApzc2);
/**
* Recursive helper function to build the APZC tree. The tree of APZC instances has
@ -285,6 +285,12 @@ private:
* is considered part of the APZC tree management state. */
mozilla::Monitor mTreeLock;
nsRefPtr<AsyncPanZoomController> mRootApzc;
/* This tracks the APZC that should receive all inputs for the current input event block.
* This allows touch points to move outside the thing they started on, but still have the
* touch events delivered to the same initial APZC. This will only ever be touched on the
* input delivery thread, and so does not require locking.
*/
nsRefPtr<AsyncPanZoomController> mApzcForInputBlock;
};
}

View File

@ -255,6 +255,7 @@ AsyncPanZoomController::Destroy()
}
mPrevSibling = nullptr;
mLastChild = nullptr;
mParent = nullptr;
}
/* static */float

View File

@ -616,13 +616,27 @@ private:
* instance.
*/
public:
void SetLastChild(AsyncPanZoomController* child) { mLastChild = child; }
void SetPrevSibling(AsyncPanZoomController* sibling) { mPrevSibling = sibling; }
void SetLastChild(AsyncPanZoomController* child) {
mLastChild = child;
if (child) {
child->mParent = this;
}
}
void SetPrevSibling(AsyncPanZoomController* sibling) {
mPrevSibling = sibling;
if (sibling) {
sibling->mParent = mParent;
}
}
AsyncPanZoomController* GetLastChild() const { return mLastChild; }
AsyncPanZoomController* GetPrevSibling() const { return mPrevSibling; }
AsyncPanZoomController* GetParent() const { return mParent; }
private:
nsRefPtr<AsyncPanZoomController> mLastChild;
nsRefPtr<AsyncPanZoomController> mPrevSibling;
nsRefPtr<AsyncPanZoomController> mParent;
/* The functions and members in this section are used to maintain the
* area that this APZC instance is responsible for. This is used when

View File

@ -381,6 +381,17 @@ NudgeToIntegers(const gfxPoint& aPoint)
return gfxPoint(x, y);
}
static already_AddRefed<AsyncPanZoomController>
GetTargetAPZC(APZCTreeManager* manager, const ScreenPoint& aPoint,
gfx3DMatrix& aTransformToApzcOut, gfx3DMatrix& aTransformToScreenOut)
{
nsRefPtr<AsyncPanZoomController> hit = manager->GetTargetAPZC(aPoint);
if (hit) {
manager->GetInputTransforms(hit.get(), aTransformToApzcOut, aTransformToScreenOut);
}
return hit.forget();
}
TEST(APZCTreeManager, GetAPZCAtPoint) {
nsTArray<nsRefPtr<Layer> > layers;
nsRefPtr<LayerManager> lm;
@ -396,7 +407,7 @@ TEST(APZCTreeManager, GetAPZCAtPoint) {
gfx3DMatrix transformToScreen;
// No APZC attached so hit testing will return no APZC at (20,20)
nsRefPtr<AsyncPanZoomController> hit = manager->GetTargetAPZC(ScreenPoint(20, 20), transformToApzc, transformToScreen);
nsRefPtr<AsyncPanZoomController> hit = GetTargetAPZC(manager, ScreenPoint(20, 20), transformToApzc, transformToScreen);
AsyncPanZoomController* nullAPZC = nullptr;
EXPECT_EQ(nullAPZC, hit.get());
EXPECT_EQ(gfx3DMatrix(), transformToApzc);
@ -405,7 +416,7 @@ TEST(APZCTreeManager, GetAPZCAtPoint) {
// Now we have a root APZC that will match the page
SetScrollableFrameMetrics(root, FrameMetrics::ROOT_SCROLL_ID, mcc);
manager->UpdatePanZoomControllerTree(nullptr, root, 0, false);
hit = manager->GetTargetAPZC(ScreenPoint(15, 15), transformToApzc, transformToScreen);
hit = GetTargetAPZC(manager, ScreenPoint(15, 15), transformToApzc, transformToScreen);
EXPECT_EQ(root->AsContainerLayer()->GetAsyncPanZoomController(), hit.get());
// expect hit point at LayerIntPoint(15, 15)
EXPECT_EQ(gfxPoint(15, 15), transformToApzc.Transform(gfxPoint(15, 15)));
@ -415,36 +426,36 @@ TEST(APZCTreeManager, GetAPZCAtPoint) {
SetScrollableFrameMetrics(layers[3], FrameMetrics::START_SCROLL_ID, mcc);
manager->UpdatePanZoomControllerTree(nullptr, root, 0, false);
EXPECT_NE(root->AsContainerLayer()->GetAsyncPanZoomController(), layers[3]->AsContainerLayer()->GetAsyncPanZoomController());
hit = manager->GetTargetAPZC(ScreenPoint(15, 15), transformToApzc, transformToScreen);
hit = GetTargetAPZC(manager, ScreenPoint(15, 15), transformToApzc, transformToScreen);
EXPECT_EQ(layers[3]->AsContainerLayer()->GetAsyncPanZoomController(), hit.get());
// expect hit point at LayerIntPoint(15, 15)
EXPECT_EQ(gfxPoint(15, 15), transformToApzc.Transform(gfxPoint(15, 15)));
EXPECT_EQ(gfxPoint(15, 15), transformToScreen.Transform(gfxPoint(15, 15)));
// Now test hit testing when we have two scrollable layers
hit = manager->GetTargetAPZC(ScreenPoint(15, 15), transformToApzc, transformToScreen);
hit = GetTargetAPZC(manager, ScreenPoint(15, 15), transformToApzc, transformToScreen);
EXPECT_EQ(layers[3]->AsContainerLayer()->GetAsyncPanZoomController(), hit.get());
SetScrollableFrameMetrics(layers[4], FrameMetrics::START_SCROLL_ID + 1, mcc);
manager->UpdatePanZoomControllerTree(nullptr, root, 0, false);
hit = manager->GetTargetAPZC(ScreenPoint(15, 15), transformToApzc, transformToScreen);
hit = GetTargetAPZC(manager, ScreenPoint(15, 15), transformToApzc, transformToScreen);
EXPECT_EQ(layers[4]->AsContainerLayer()->GetAsyncPanZoomController(), hit.get());
// expect hit point at LayerIntPoint(15, 15)
EXPECT_EQ(gfxPoint(15, 15), transformToApzc.Transform(gfxPoint(15, 15)));
EXPECT_EQ(gfxPoint(15, 15), transformToScreen.Transform(gfxPoint(15, 15)));
// Hit test ouside the reach of layer[3,4] but inside root
hit = manager->GetTargetAPZC(ScreenPoint(90, 90), transformToApzc, transformToScreen);
hit = GetTargetAPZC(manager, ScreenPoint(90, 90), transformToApzc, transformToScreen);
EXPECT_EQ(root->AsContainerLayer()->GetAsyncPanZoomController(), hit.get());
// expect hit point at LayerIntPoint(90, 90)
EXPECT_EQ(gfxPoint(90, 90), transformToApzc.Transform(gfxPoint(90, 90)));
EXPECT_EQ(gfxPoint(90, 90), transformToScreen.Transform(gfxPoint(90, 90)));
// Hit test ouside the reach of any layer
hit = manager->GetTargetAPZC(ScreenPoint(1000, 10), transformToApzc, transformToScreen);
hit = GetTargetAPZC(manager, ScreenPoint(1000, 10), transformToApzc, transformToScreen);
EXPECT_EQ(nullAPZC, hit.get());
EXPECT_EQ(gfx3DMatrix(), transformToApzc);
EXPECT_EQ(gfx3DMatrix(), transformToScreen);
hit = manager->GetTargetAPZC(ScreenPoint(-1000, 10), transformToApzc, transformToScreen);
hit = GetTargetAPZC(manager, ScreenPoint(-1000, 10), transformToApzc, transformToScreen);
EXPECT_EQ(nullAPZC, hit.get());
EXPECT_EQ(gfx3DMatrix(), transformToApzc);
EXPECT_EQ(gfx3DMatrix(), transformToScreen);
@ -454,14 +465,14 @@ TEST(APZCTreeManager, GetAPZCAtPoint) {
transform.ScalePost(0.1, 0.1, 1);
root->SetBaseTransform(transform);
manager->UpdatePanZoomControllerTree(nullptr, root, 0, false);
hit = manager->GetTargetAPZC(ScreenPoint(50, 50), transformToApzc, transformToScreen); // This point is now outside the root layer
hit = GetTargetAPZC(manager, ScreenPoint(50, 50), transformToApzc, transformToScreen); // This point is now outside the root layer
EXPECT_EQ(nullAPZC, hit.get());
EXPECT_EQ(gfx3DMatrix(), transformToApzc);
EXPECT_EQ(gfx3DMatrix(), transformToScreen);
// This hit test will hit both layers[3] and layers[4]; layers[4] is later in the tree so
// it is a better match
hit = manager->GetTargetAPZC(ScreenPoint(2, 2), transformToApzc, transformToScreen);
hit = GetTargetAPZC(manager, ScreenPoint(2, 2), transformToApzc, transformToScreen);
EXPECT_EQ(layers[4]->AsContainerLayer()->GetAsyncPanZoomController(), hit.get());
// expect hit point at LayerPoint(20, 20)
EXPECT_EQ(gfxPoint(20, 20), NudgeToIntegers(transformToApzc.Transform(gfxPoint(2, 2))));
@ -472,7 +483,7 @@ TEST(APZCTreeManager, GetAPZCAtPoint) {
// layer 4 effective visible screenrect: (0.05, 0.05, 0.2, 0.2)
// Does not contain (2, 2)
manager->UpdatePanZoomControllerTree(nullptr, root, 0, false);
hit = manager->GetTargetAPZC(ScreenPoint(2, 2), transformToApzc, transformToScreen);
hit = GetTargetAPZC(manager, ScreenPoint(2, 2), transformToApzc, transformToScreen);
EXPECT_EQ(layers[3]->AsContainerLayer()->GetAsyncPanZoomController(), hit.get());
// expect hit point at LayerPoint(20, 20)
EXPECT_EQ(gfxPoint(20, 20), NudgeToIntegers(transformToApzc.Transform(gfxPoint(2, 2))));
@ -495,7 +506,7 @@ TEST(APZCTreeManager, GetAPZCAtPoint) {
manager->UpdatePanZoomControllerTree(nullptr, root, 0, false);
// layer 7 effective visible screenrect (0,16,4,60) but clipped by parent layers
hit = manager->GetTargetAPZC(ScreenPoint(1, 45), transformToApzc, transformToScreen);
hit = GetTargetAPZC(manager, ScreenPoint(1, 45), transformToApzc, transformToScreen);
EXPECT_EQ(layers[7]->AsContainerLayer()->GetAsyncPanZoomController(), hit.get());
// expect hit point at LayerPoint(20, 440), which is CSSPoint(20, 29)
EXPECT_EQ(gfxPoint(20, 440), NudgeToIntegers(transformToApzc.Transform(gfxPoint(1, 45))));

View File

@ -38,9 +38,9 @@ public class BrowserDB {
public Cursor filter(ContentResolver cr, CharSequence constraint, int limit);
// This should onlyl return frecent sites, BrowserDB.getTopSites will do the
// This should only return frecent bookmarks, BrowserDB.getTopBookmarks will do the
// work to combine that list with the pinned sites list
public Cursor getTopSites(ContentResolver cr, int limit);
public Cursor getTopBookmarks(ContentResolver cr, int limit);
public void updateVisitedHistory(ContentResolver cr, String uri);
@ -137,12 +137,12 @@ public class BrowserDB {
return sDb.filter(cr, constraint, limit);
}
public static Cursor getTopSites(ContentResolver cr, int limit) {
// Note this is not a single query anymore, but actually returns a mixture of two queries, one for topSites
// and one for pinned sites
Cursor topSites = sDb.getTopSites(cr, limit);
public static Cursor getTopBookmarks(ContentResolver cr, int limit) {
// Note this is not a single query anymore, but actually returns a mixture of two queries,
// one for top bookmarks, and one for pinned sites (which are actually bookmarks as well).
Cursor topBookmarks = sDb.getTopBookmarks(cr, limit);
Cursor pinnedSites = sDb.getPinnedSites(cr, limit);
return new TopSitesCursorWrapper(pinnedSites, topSites, limit);
return new TopSitesCursorWrapper(pinnedSites, topBookmarks, limit);
}
public static void updateVisitedHistory(ContentResolver cr, String uri) {

View File

@ -231,9 +231,13 @@ public class LocalBrowserDB implements BrowserDB.BrowserDBIface {
}
@Override
public Cursor getTopSites(ContentResolver cr, int limit) {
// Filter out sites that are pinned
String selection = DBUtils.concatenateWhere("", Combined.URL + " NOT IN (SELECT " +
public Cursor getTopBookmarks(ContentResolver cr, int limit) {
// Only select bookmarks. Unfortunately, we need to query the combined view,
// instead of just the bookmarks table, in order to do the frecency calculation.
String selection = Combined.BOOKMARK_ID + " IS NOT NULL";
// Filter out sites that are pinned.
selection = DBUtils.concatenateWhere(selection, Combined.URL + " NOT IN (SELECT " +
Bookmarks.URL + " FROM bookmarks WHERE " +
DBUtils.qualifyColumn("bookmarks", Bookmarks.PARENT) + " == ? AND " +
DBUtils.qualifyColumn("bookmarks", Bookmarks.IS_DELETED) + " == 0)");

View File

@ -60,13 +60,18 @@ class BookmarksListAdapter extends MultiTypeCursorAdapter {
/**
* Moves to parent folder, if one exists.
*
* @return Whether the adapter successfully moved to a parent folder.
*/
public void moveToParentFolder() {
public boolean moveToParentFolder() {
// If we're already at the root, we can't move to a parent folder
if (mParentStack.size() != 1) {
mParentStack.removeFirst();
refreshCurrentFolder();
if (mParentStack.size() == 1) {
return false;
}
mParentStack.removeFirst();
refreshCurrentFolder();
return true;
}
/**

View File

@ -13,6 +13,7 @@ import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import android.content.Context;
import android.database.Cursor;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
@ -56,6 +57,17 @@ public class BookmarksListView extends HomeListView
super.onAttachedToWindow();
setOnItemClickListener(this);
setOnKeyListener(new View.OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
// If the user hit the BACK key, try to move to the parent folder.
if (keyCode == KeyEvent.KEYCODE_BACK) {
return getBookmarksListAdapter().moveToParentFolder();
}
return false;
}
});
}
@Override
@ -101,14 +113,7 @@ public class BookmarksListView extends HomeListView
// Absolute position for the adapter.
position -= headerCount;
BookmarksListAdapter adapter;
ListAdapter listAdapter = getAdapter();
if (listAdapter instanceof HeaderViewListAdapter) {
adapter = (BookmarksListAdapter) ((HeaderViewListAdapter) listAdapter).getWrappedAdapter();
} else {
adapter = (BookmarksListAdapter) listAdapter;
}
final BookmarksListAdapter adapter = getBookmarksListAdapter();
if (adapter.isShowingChildFolder()) {
if (position == 0) {
// If we tap on an opened folder, move back to parent folder.
@ -141,4 +146,15 @@ public class BookmarksListView extends HomeListView
getOnUrlOpenListener().onUrlOpen(url, EnumSet.of(OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
}
}
private BookmarksListAdapter getBookmarksListAdapter() {
BookmarksListAdapter adapter;
ListAdapter listAdapter = getAdapter();
if (listAdapter instanceof HeaderViewListAdapter) {
adapter = (BookmarksListAdapter) ((HeaderViewListAdapter) listAdapter).getWrappedAdapter();
} else {
adapter = (BookmarksListAdapter) listAdapter;
}
return adapter;
}
}

View File

@ -149,6 +149,10 @@ public class BookmarksPage extends HomeFragment {
});
mList.setAdapter(mListAdapter);
// Invalidate the cached value that keeps track of whether or
// not desktop bookmarks (or reading list items) exist.
BrowserDB.invalidateCachedState();
// Create callbacks before the initial loader is started.
mLoaderCallbacks = new CursorLoaderCallbacks(activity, getLoaderManager());
mThumbnailsLoaderCallbacks = new ThumbnailsLoaderCallbacks();
@ -362,7 +366,7 @@ public class BookmarksPage extends HomeFragment {
@Override
public Cursor loadCursor() {
final int max = getContext().getResources().getInteger(R.integer.number_of_top_sites);
return BrowserDB.getTopSites(getContext().getContentResolver(), max);
return BrowserDB.getTopBookmarks(getContext().getContentResolver(), max);
}
}

View File

@ -6,6 +6,7 @@
package org.mozilla.gecko.home;
import org.mozilla.gecko.EditBookmarkDialog;
import org.mozilla.gecko.Favicons;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoEvent;
import org.mozilla.gecko.R;
@ -132,14 +133,8 @@ abstract class HomeFragment extends Fragment {
return false;
}
// FIXME: bug 897772
Bitmap bitmap = null;
if (info.favicon != null) {
bitmap = BitmapUtils.decodeByteArray(info.favicon);
}
String shortcutTitle = TextUtils.isEmpty(info.title) ? info.url.replaceAll(REGEX_URL_TO_TITLE, "") : info.title;
GeckoAppShell.createShortcut(shortcutTitle, info.url, bitmap, "");
new AddToLauncherTask(info.url, shortcutTitle).execute();
return true;
}
@ -225,6 +220,35 @@ abstract class HomeFragment extends Fragment {
}
}
private static class AddToLauncherTask extends UiAsyncTask<Void, Void, String> {
private final String mUrl;
private final String mTitle;
public AddToLauncherTask(String url, String title) {
super(ThreadUtils.getBackgroundHandler());
mUrl = url;
mTitle = title;
}
@Override
public String doInBackground(Void... params) {
return Favicons.getInstance().getFaviconUrlForPageUrl(mUrl);
}
@Override
public void onPostExecute(String faviconUrl) {
Favicons.OnFaviconLoadedListener listener = new Favicons.OnFaviconLoadedListener() {
@Override
public void onFaviconLoaded(String url, Bitmap favicon) {
GeckoAppShell.createShortcut(mTitle, mUrl, favicon, "");
}
};
Favicons.getInstance().loadFavicon(mUrl, faviconUrl, 0, listener);
}
}
private static class RemoveBookmarkTask extends UiAsyncTask<Void, Void, Void> {
private final Context mContext;
private final int mId;

View File

@ -113,7 +113,6 @@ public class HomeListView extends ListView
public int bookmarkId;
public int historyId;
public String url;
public byte[] favicon;
public String title;
public int display;
public boolean isFolder;
@ -165,13 +164,6 @@ public class HomeListView extends ListView
historyId = -1;
}
final int faviconCol = cursor.getColumnIndex(Combined.FAVICON);
if (faviconCol != -1) {
favicon = cursor.getBlob(faviconCol);
} else {
favicon = null;
}
// We only have the parent column in cursors from getBookmarksInFolder.
final int parentCol = cursor.getColumnIndex(Bookmarks.PARENT);
if (parentCol != -1) {

View File

@ -587,11 +587,11 @@ AboutReader.prototype = {
let start = 0;
if (host.startsWith("www"))
if (host.startsWith("www."))
start = 4;
else if (host.startsWith("m"))
else if (host.startsWith("m."))
start = 2;
else if (host.startsWith("mobile"))
else if (host.startsWith("mobile."))
start = 7;
return host.substring(start);

View File

@ -50,6 +50,9 @@ if (outOfProcess) {
mm.loadFrameScript(CHILD_LOGGER_SCRIPT, true);
mm.loadFrameScript(CHILD_SCRIPT_API, true);
mm.loadFrameScript(CHILD_SCRIPT, true);
//Workaround for bug 848411, once that bug is fixed, the following line can be removed
mm.loadFrameScript('data:,addEventListener%28%22DOMWindowCreated%22%2C%20function%28e%29%20%7B%0A%20%20removeEventListener%28%22DOMWindowCreated%22%2C%20arguments.callee%2C%20false%29%3B%0A%20%20var%20window%20%3D%20e.target.defaultView%3B%0A%20%20window.wrappedJSObject.SpecialPowers.addPermission%28%22allowXULXBL%22%2C%20true%2C%20window.document%29%3B%0A%7D%0AB', true);
specialPowersObserver._isFrameScriptLoaded = true;
}

View File

@ -49,7 +49,7 @@ function injectController(doc, topic, data) {
return;
}
var containingBrowser = window.QueryInterface(Ci.nsIInterfaceRequestor)
let containingBrowser = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell)
.chromeEventHandler;
@ -67,7 +67,7 @@ function injectController(doc, topic, data) {
}
SocialService.getProvider(doc.nodePrincipal.origin, function(provider) {
if (provider && provider.workerURL && provider.enabled) {
if (provider && provider.enabled) {
attachToWindow(provider, window);
}
});
@ -88,7 +88,7 @@ function attachToWindow(provider, targetWindow) {
return;
}
var port = provider.getWorkerPort(targetWindow);
let port = provider.workerURL ? provider.getWorkerPort(targetWindow) : null;
let mozSocialObj = {
// Use a method for backwards compat with existing providers, but we
@ -206,12 +206,14 @@ function attachToWindow(provider, targetWindow) {
return targetWindow.navigator.wrappedJSObject.mozSocial = contentObj;
});
targetWindow.addEventListener("unload", function () {
// We want to close the port, but also want the target window to be
// able to use the port during an unload event they setup - so we
// set a timer which will fire after the unload events have all fired.
schedule(function () { port.close(); });
});
if (port) {
targetWindow.addEventListener("unload", function () {
// We want to close the port, but also want the target window to be
// able to use the port during an unload event they setup - so we
// set a timer which will fire after the unload events have all fired.
schedule(function () { port.close(); });
});
}
// We allow window.close() to close the panel, so add an event handler for
// this, then cancel the event (so the window itself doesn't die) and

View File

@ -34,7 +34,13 @@ XPCOMUtils.defineLazyServiceGetter(this, "etld",
// Internal helper methods and state
let SocialServiceInternal = {
enabled: Services.prefs.getBoolPref("social.enabled"),
_enabled: Services.prefs.getBoolPref("social.enabled"),
get enabled() this._enabled,
set enabled(val) {
this._enabled = !!val;
Services.prefs.setBoolPref("social.enabled", !!val);
},
get providerArray() {
return [p for ([, p] of Iterator(this.providers))];
},
@ -362,7 +368,6 @@ this.SocialService = {
SocialServiceInternal.enabled = enable;
MozSocialAPI.enabled = enable;
Services.obs.notifyObservers(null, "social:pref-changed", enable ? "enabled" : "disabled");
Services.telemetry.getHistogramById("SOCIAL_TOGGLED").add(enable);
},
@ -723,6 +728,12 @@ function SocialProvider(input) {
}
SocialProvider.prototype = {
reload: function() {
this._terminate();
this._activate();
Services.obs.notifyObservers(null, "social:provider-reload", this.origin);
},
// Provider enabled/disabled state. Disabled providers do not have active
// connections to their FrameWorkers.
_enabled: false,
@ -868,6 +879,10 @@ SocialProvider.prototype = {
closeAllChatWindows(this);
},
haveLoggedInUser: function () {
return !!(this.profile && this.profile.userName);
},
// Called by the workerAPI to add/update a notification icon.
setAmbientNotification: function(notification) {
if (!this.profile.userName)

View File

@ -2,6 +2,8 @@
* 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/. */
let provider;
function test() {
waitForExplicitFinish();
@ -13,10 +15,22 @@ function test() {
ensureSocialEnabled();
SocialService.addProvider(manifest, function (provider) {
// enable the provider
provider.enabled = true;
SocialService.addProvider(manifest, function (p) {
provider = p;
runTests(tests, undefined, undefined, function () {
SocialService.removeProvider(p.origin, function() {
ok(!provider.enabled, "removing an enabled provider should have disabled the provider");
let port = provider.getWorkerPort();
ok(!port, "should not be able to get a port after removing the provider");
provider = null;
finish();
});
});
});
}
let tests = {
testSingleProvider: function(next) {
ok(provider.enabled, "provider is initially enabled");
let port = provider.getWorkerPort();
ok(port, "should be able to get a port from enabled provider");
@ -37,12 +51,38 @@ function test() {
ok(port, "should be able to get a port from re-enabled provider");
port.close();
ok(provider.workerAPI, "should be able to get a workerAPI from re-enabled provider");
SocialService.removeProvider(provider.origin, function() {
ok(!provider.enabled, "removing an enabled provider should have disabled the provider");
next();
},
testTwoProviders: function(next) {
// add another provider, test both workers
let manifest = {
origin: 'http://test2.example.com',
name: "Example Provider 2",
workerURL: "http://test2.example.com/browser/toolkit/components/social/test/browser/worker_social.js"
};
SocialService.addProvider(manifest, function (provider2) {
ok(provider.enabled, "provider is initially enabled");
ok(!provider2.enabled, "provider2 is not initially enabled");
provider2.enabled = true;
let port = provider.getWorkerPort();
ok(!port, "should not be able to get a port after removing the provider");
finish();
let port2 = provider2.getWorkerPort();
ok(port, "have port for provider");
ok(port2, "have port for provider2");
port.onmessage = function(e) {
if (e.data.topic == "test-initialization-complete") {
ok(true, "first provider initialized");
port2.postMessage({topic: "test-initialization"});
}
}
port2.onmessage = function(e) {
if (e.data.topic == "test-initialization-complete") {
ok(true, "second provider initialized");
SocialService.removeProvider(provider2.origin, function() {
next();
});
}
}
port.postMessage({topic: "test-initialization"});
});
});
}
}

View File

@ -78,38 +78,44 @@ function testGetProviderList(manifests, next) {
}
}
function testEnabled(manifests, next) {
// Check that providers are disabled by default
let providers = yield SocialService.getProviderList(next);
do_check_true(providers.length >= manifests.length);
do_check_true(SocialService.enabled);
do_check_true(SocialService.enabled, "social enabled at test start");
providers.forEach(function (provider) {
do_check_false(provider.enabled);
});
let notificationDisabledCorrect = false;
Services.obs.addObserver(function obs1(subj, topic, data) {
Services.obs.removeObserver(obs1, "social:pref-changed");
notificationDisabledCorrect = data == "disabled";
}, "social:pref-changed", false);
do_test_pending();
function waitForEnableObserver(cb) {
Services.prefs.addObserver("social.enabled", function prefObserver(subj, topic, data) {
Services.prefs.removeObserver("social.enabled", prefObserver);
cb();
}, false);
}
// enable one of the added providers
providers[providers.length-1].enabled = true;
// now disable the service and check that it disabled that provider (and all others for good measure)
waitForEnableObserver(function() {
do_check_true(!SocialService.enabled);
providers.forEach(function (provider) {
do_check_false(provider.enabled);
});
waitForEnableObserver(function() {
do_check_true(SocialService.enabled);
// Enabling the service should not enable providers
providers.forEach(function (provider) {
do_check_false(provider.enabled);
});
do_test_finished();
});
SocialService.enabled = true;
});
SocialService.enabled = false;
do_check_true(notificationDisabledCorrect);
do_check_true(!SocialService.enabled);
providers.forEach(function (provider) {
do_check_false(provider.enabled);
});
SocialService.enabled = true;
do_check_true(SocialService.enabled);
// Enabling the service should not enable providers
providers.forEach(function (provider) {
do_check_false(provider.enabled);
});
}
function testAddRemoveProvider(manifests, next) {