Merge mozilla-central to mozilla-inbound

This commit is contained in:
Carsten "Tomcat" Book 2014-12-18 13:54:52 +01:00
commit ad4a8588eb
100 changed files with 2201 additions and 671 deletions

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="3ab0d9c70f0b2e1ededc679112c392303f037361">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="7a865bd6163e9a0372861d27450b3434875ef5c1"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="58734e8a48157f99d5b733412b600c2e04c954fe"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="50ad16a280fe9cfa0716f8c6ba16afdf7f266b49"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>

View File

@ -19,7 +19,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="7a865bd6163e9a0372861d27450b3434875ef5c1"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="58734e8a48157f99d5b733412b600c2e04c954fe"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="50ad16a280fe9cfa0716f8c6ba16afdf7f266b49"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="d5d3f93914558b6f168447b805cd799c8233e300"/>

View File

@ -17,7 +17,7 @@
</project>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="7a865bd6163e9a0372861d27450b3434875ef5c1"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="58734e8a48157f99d5b733412b600c2e04c954fe"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="50ad16a280fe9cfa0716f8c6ba16afdf7f266b49"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="f0e6d8bde961683b7862b4eb0bb04c49d9699f3c"/>

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="3ab0d9c70f0b2e1ededc679112c392303f037361">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="7a865bd6163e9a0372861d27450b3434875ef5c1"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="58734e8a48157f99d5b733412b600c2e04c954fe"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="50ad16a280fe9cfa0716f8c6ba16afdf7f266b49"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>

View File

@ -19,7 +19,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="7a865bd6163e9a0372861d27450b3434875ef5c1"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="58734e8a48157f99d5b733412b600c2e04c954fe"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="50ad16a280fe9cfa0716f8c6ba16afdf7f266b49"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="d5d3f93914558b6f168447b805cd799c8233e300"/>

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="3ab0d9c70f0b2e1ededc679112c392303f037361">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="7a865bd6163e9a0372861d27450b3434875ef5c1"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="58734e8a48157f99d5b733412b600c2e04c954fe"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="50ad16a280fe9cfa0716f8c6ba16afdf7f266b49"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>

View File

@ -17,7 +17,7 @@
</project>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="7a865bd6163e9a0372861d27450b3434875ef5c1"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="58734e8a48157f99d5b733412b600c2e04c954fe"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="50ad16a280fe9cfa0716f8c6ba16afdf7f266b49"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="f0e6d8bde961683b7862b4eb0bb04c49d9699f3c"/>

View File

@ -4,6 +4,6 @@
"remote": "",
"branch": ""
},
"revision": "fbcff6610f8b99726ef14b0c6d894b4a7053eecf",
"revision": "d937413f736efd995436c211649f930191429b66",
"repo_path": "integration/gaia-central"
}

View File

@ -17,7 +17,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="7a865bd6163e9a0372861d27450b3434875ef5c1"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="58734e8a48157f99d5b733412b600c2e04c954fe"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="50ad16a280fe9cfa0716f8c6ba16afdf7f266b49"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>

View File

@ -15,7 +15,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="7a865bd6163e9a0372861d27450b3434875ef5c1"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="58734e8a48157f99d5b733412b600c2e04c954fe"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="50ad16a280fe9cfa0716f8c6ba16afdf7f266b49"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>

View File

@ -17,7 +17,7 @@
</project>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="7a865bd6163e9a0372861d27450b3434875ef5c1"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="58734e8a48157f99d5b733412b600c2e04c954fe"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="50ad16a280fe9cfa0716f8c6ba16afdf7f266b49"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="fe893bb760a3bb64375f62fdf4762a58c59df9ef"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="f0e6d8bde961683b7862b4eb0bb04c49d9699f3c"/>

View File

@ -17,7 +17,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="7a865bd6163e9a0372861d27450b3434875ef5c1"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="58734e8a48157f99d5b733412b600c2e04c954fe"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="50ad16a280fe9cfa0716f8c6ba16afdf7f266b49"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="827214fcf38d6569aeb5c6d6f31cb296d1f09272"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="891e5069c0ad330d8191bf8c7b879c814258c89f"/>

View File

@ -1663,6 +1663,7 @@ pref("loop.server", "https://loop.services.mozilla.com/v0");
pref("loop.seenToS", "unseen");
pref("loop.gettingStarted.seen", false);
pref("loop.gettingStarted.url", "https://www.mozilla.org/%LOCALE%/firefox/%VERSION%/hello/start");
pref("loop.gettingStarted.resumeOnFirstJoin", false);
pref("loop.learnMoreUrl", "https://www.firefox.com/hello/");
pref("loop.legal.ToS_url", "https://www.mozilla.org/about/legal/terms/firefox-hello/");
pref("loop.legal.privacy_url", "https://www.mozilla.org/privacy/firefox-hello/");

View File

@ -331,7 +331,9 @@ let gFxAccounts = {
fxaMigrator.createFxAccount(window);
break;
case "migrate-verify":
fxaMigrator.resendVerificationMail();
// Instead of using the migrator module directly here the UX calls for
// us to open prefs which has a "resend" button.
this.openPreferences();
break;
default:
this.openAccountsPage(null, { entryPoint: "menupanel" });

View File

@ -6,6 +6,7 @@
let LoopUI;
XPCOMUtils.defineLazyModuleGetter(this, "injectLoopAPI", "resource:///modules/loop/MozLoopAPI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "LoopRooms", "resource:///modules/loop/LoopRooms.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "MozLoopService", "resource:///modules/loop/MozLoopService.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PanelFrame", "resource:///modules/PanelFrame.jsm");
@ -84,8 +85,70 @@ XPCOMUtils.defineLazyModuleGetter(this, "PanelFrame", "resource:///modules/Panel
// Used to clear the temporary "login" state from the button.
Services.obs.notifyObservers(null, "loop-status-changed", null);
PanelFrame.showPopup(window, event ? event.target : this.toolbarButton.node,
"loop", null, "about:looppanel", null, callback);
this.shouldResumeTour().then((resume) => {
if (resume) {
// Assume the conversation with the visitor wasn't open since we would
// have resumed the tour as soon as the visitor joined if it was (and
// the pref would have been set to false already.
MozLoopService.resumeTour("waiting");
resolve();
return;
}
PanelFrame.showPopup(window, event ? event.target : this.toolbarButton.node,
"loop", null, "about:looppanel", null, callback);
});
});
},
/**
* Method to know whether actions to open the panel should instead resume the tour.
*
* We need the panel to be opened via UITour so that it gets @noautohide.
*
* @return {Promise} resolving with a {Boolean} of whether the tour should be resumed instead of
* opening the panel.
*/
shouldResumeTour: Task.async(function* () {
// Resume the FTU tour if this is the first time a room was joined by
// someone else since the tour.
if (!Services.prefs.getBoolPref("loop.gettingStarted.resumeOnFirstJoin")) {
return false;
}
if (!LoopRooms.participantsCount) {
// Nobody is in the rooms
return false;
}
let roomsWithNonOwners = yield this.roomsWithNonOwners();
if (!roomsWithNonOwners.length) {
// We were the only one in a room but we want to know about someone else joining.
return false;
}
return true;
}),
/**
* @return {Promise} resolved with an array of Rooms with participants (excluding owners)
*/
roomsWithNonOwners: function() {
return new Promise(resolve => {
LoopRooms.getAll((error, rooms) => {
let roomsWithNonOwners = [];
for (let room of rooms) {
if (!("participants" in room)) {
continue;
}
let numNonOwners = room.participants.filter(participant => !participant.owner).length;
if (!numNonOwners) {
continue;
}
roomsWithNonOwners.push(room);
}
resolve(roomsWithNonOwners);
});
});
},

View File

@ -7476,6 +7476,7 @@ function switchToTabHavingURI(aURI, aOpenNew, aOpenParams={}) {
// Certain URLs can be switched to irrespective of the source or destination
// window being in private browsing mode:
const kPrivateBrowsingWhitelist = new Set([
"about:addons",
"about:customizing",
]);
@ -7737,7 +7738,8 @@ XPCOMUtils.defineLazyGetter(ResponsiveUI, "ResponsiveUIManager", function() {
});
function openEyedropper() {
var eyedropper = new this.Eyedropper(this);
var eyedropper = new this.Eyedropper(this, { context: "menu",
copyOnSelect: true });
eyedropper.open();
}

View File

@ -486,3 +486,4 @@ skip-if = e10s # bug 1100687 - test directly manipulates content (content.docume
[browser_mcb_redirect.js]
skip-if = e10s # bug 1084504 - [e10s] Mixed content detection does not take redirection into account
[browser_windowactivation.js]
[browser_bug963945.js]

View File

@ -0,0 +1,30 @@
/* 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/. */
/*
* This test ensures the about:addons tab is only
* opened one time when in private browsing.
*/
function test() {
waitForExplicitFinish();
var win = OpenBrowserWindow({private: true});
whenDelayedStartupFinished(win, function() {
win.gBrowser.loadURI("about:addons");
waitForFocus(function() {
EventUtils.synthesizeKey("a", { ctrlKey: true, shiftKey: true }, win);
is(win.gBrowser.tabs.length, 1, "about:addons tab was re-focused.");
is(win.gBrowser.currentURI.spec, "about:addons", "Addons tab was opened.");
win.close();
finish();
});
});
}

View File

@ -939,12 +939,24 @@
this.closePopup();
controller.handleEscape();
// Fill in the search bar's value
searchBar.value = search;
// open the search results according to the clicking subtlety
var where = whereToOpenLink(aEvent, false, true);
// But open ctrl/cmd clicks on autocomplete items in a new background tab.
if (where == "tab" && (aEvent instanceof MouseEvent) &&
(aEvent.button == 1 ||
#ifdef XP_MACOSX
aEvent.metaKey))
#else
aEvent.ctrlKey))
#endif
where = "tab-background";
searchBar.doSearch(search, where);
if (where == "tab-background")
searchBar.focus();
else
searchBar.value = search;
}
]]></body>
</method>

View File

@ -155,7 +155,7 @@ let LoopRoomsInternal = {
* `Error` object or `null`. The second argument will
* be the list of rooms, if it was fetched successfully.
*/
getAll: function(version = null, callback) {
getAll: function(version = null, callback = null) {
if (!callback) {
callback = version;
version = null;

View File

@ -1099,6 +1099,33 @@ this.MozLoopService = {
}
});
// Resume the tour (re-opening the tab, if necessary) if someone else joins
// a room of ours and it's currently open.
LoopRooms.on("joined", (e, room, participant) => {
let isOwnerInRoom = false;
let isOtherInRoom = false;
if (!room.participants) {
return;
}
// The particpant that joined isn't necessarily included in room.participants (depending on
// when the broadcast happens) so concatenate.
for (let participant of room.participants.concat(participant)) {
if (participant.owner) {
isOwnerInRoom = true;
} else {
isOtherInRoom = true;
}
}
if (!isOwnerInRoom || !isOtherInRoom) {
return;
}
this.resumeTour("open");
});
// If expiresTime is not in the future and the user hasn't
// previously authenticated then skip registration.
if (!MozLoopServiceInternal.urlExpiryTimeIsInFuture() &&
@ -1476,23 +1503,64 @@ this.MozLoopService = {
}
}),
/**
* Gets the tour URL.
*
* @param {String} aSrc A string representing the entry point to begin the tour, optional.
* @param {Object} aAdditionalParams An object with keys used as query parameter names
*/
getTourURL: function(aSrc = null, aAdditionalParams = {}) {
let urlStr = this.getLoopPref("gettingStarted.url");
let url = new URL(Services.urlFormatter.formatURL(urlStr));
for (let paramName in aAdditionalParams) {
url.searchParams.append(paramName, aAdditionalParams[paramName]);
}
if (aSrc) {
url.searchParams.set("utm_source", "firefox-browser");
url.searchParams.set("utm_medium", "firefox-browser");
url.searchParams.set("utm_campaign", aSrc);
}
return url;
},
resumeTour: function(aIncomingConversationState) {
let url = this.getTourURL("resume-with-conversation", {
incomingConversation: aIncomingConversationState,
});
let win = Services.wm.getMostRecentWindow("navigator:browser");
this.setLoopPref("gettingStarted.resumeOnFirstJoin", false);
// The query parameters of the url can vary but we always want to re-use a Loop tour tab that's
// already open so we ignore the fragment and query string.
let hadExistingTab = win.switchToTabHavingURI(url, true, {
ignoreFragment: true,
ignoreQueryString: true,
});
// If the tab was already open, send an event instead of using the query
// parameter above (that we don't replace on existing tabs to avoid a reload).
if (hadExistingTab) {
UITour.notify("Loop:IncomingConversation", {
conversationOpen: aIncomingConversationState === "open",
});
}
},
/**
* Opens the Getting Started tour in the browser.
*
* @param {String} aSrc
* - The UI element that the user used to begin the tour, optional.
* @param {String} [aSrc] A string representing the entry point to begin the tour, optional.
*/
openGettingStartedTour: Task.async(function(aSrc = null) {
try {
let urlStr = Services.prefs.getCharPref("loop.gettingStarted.url");
let url = new URL(Services.urlFormatter.formatURL(urlStr));
if (aSrc) {
url.searchParams.set("utm_source", "firefox-browser");
url.searchParams.set("utm_medium", "firefox-browser");
url.searchParams.set("utm_campaign", aSrc);
}
let url = this.getTourURL(aSrc);
let win = Services.wm.getMostRecentWindow("navigator:browser");
win.switchToTabHavingURI(url, true, {replaceQueryString: true});
win.switchToTabHavingURI(url, true, {
ignoreFragment: true,
replaceQueryString: true,
});
} catch (ex) {
log.error("Error opening Getting Started tour", ex);
}

View File

@ -166,11 +166,14 @@ loop.panel = (function(_, mozL10n) {
});
var GettingStartedView = React.createClass({displayName: 'GettingStartedView',
mixins: [sharedMixins.WindowCloseMixin],
handleButtonClick: function() {
navigator.mozLoop.openGettingStartedTour("getting-started");
navigator.mozLoop.setLoopPref("gettingStarted.seen", true);
var event = new CustomEvent("GettingStartedSeen");
window.dispatchEvent(event);
this.closeWindow();
},
render: function() {
@ -269,7 +272,7 @@ loop.panel = (function(_, mozL10n) {
* Panel settings (gear) menu.
*/
var SettingsDropdown = React.createClass({displayName: 'SettingsDropdown',
mixins: [sharedMixins.DropdownMenuMixin],
mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.WindowCloseMixin],
handleClickSettingsEntry: function() {
// XXX to be implemented at the same time as unhiding the entry
@ -300,6 +303,7 @@ loop.panel = (function(_, mozL10n) {
openGettingStartedTour: function() {
navigator.mozLoop.openGettingStartedTour("settings-menu");
this.closeWindow();
},
render: function() {

View File

@ -166,11 +166,14 @@ loop.panel = (function(_, mozL10n) {
});
var GettingStartedView = React.createClass({
mixins: [sharedMixins.WindowCloseMixin],
handleButtonClick: function() {
navigator.mozLoop.openGettingStartedTour("getting-started");
navigator.mozLoop.setLoopPref("gettingStarted.seen", true);
var event = new CustomEvent("GettingStartedSeen");
window.dispatchEvent(event);
this.closeWindow();
},
render: function() {
@ -269,7 +272,7 @@ loop.panel = (function(_, mozL10n) {
* Panel settings (gear) menu.
*/
var SettingsDropdown = React.createClass({
mixins: [sharedMixins.DropdownMenuMixin],
mixins: [sharedMixins.DropdownMenuMixin, sharedMixins.WindowCloseMixin],
handleClickSettingsEntry: function() {
// XXX to be implemented at the same time as unhiding the entry
@ -300,6 +303,7 @@ loop.panel = (function(_, mozL10n) {
openGettingStartedTour: function() {
navigator.mozLoop.openGettingStartedTour("settings-menu");
this.closeWindow();
},
render: function() {

View File

@ -89,6 +89,9 @@ let gSubDialog = {
this._overlay.style.visibility = "";
// Clear the sizing inline styles.
this._frame.removeAttribute("style");
// Clear the sizing attributes
this._box.removeAttribute("width");
this._box.removeAttribute("height");
setTimeout(() => {
// Unload the dialog after the event listeners run so that the load of about:blank isn't

View File

@ -9,6 +9,12 @@ XPCOMUtils.defineLazyGetter(this, "FxAccountsCommon", function () {
return Components.utils.import("resource://gre/modules/FxAccountsCommon.js", {});
});
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
"resource://gre/modules/FxAccounts.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "fxaMigrator",
"resource://services-sync/FxaMigrator.jsm");
const PAGE_NO_ACCOUNT = 0;
const PAGE_HAS_ACCOUNT = 1;
const PAGE_NEEDS_UPDATE = 2;
@ -25,7 +31,6 @@ const FXA_LOGIN_UNVERIFIED = 1;
const FXA_LOGIN_FAILED = 2;
let gSyncPane = {
_stringBundle: null,
prefArray: ["engine.bookmarks", "engine.passwords", "engine.prefs",
"engine.tabs", "engine.history"],
@ -91,6 +96,7 @@ let gSyncPane = {
"weave:service:setup-complete",
"weave:service:logout:finish",
FxAccountsCommon.ONVERIFIED_NOTIFICATION];
let migrateTopic = "fxa-migration:state-changed";
// Add the observers now and remove them on unload
//XXXzpao This should use Services.obs.* but Weave's Obs does nice handling
@ -98,14 +104,30 @@ let gSyncPane = {
topics.forEach(function (topic) {
Weave.Svc.Obs.add(topic, this.updateWeavePrefs, this);
}, this);
// The FxA migration observer is a special case.
Weave.Svc.Obs.add(migrateTopic, this.updateMigrationState, this);
window.addEventListener("unload", function() {
topics.forEach(function (topic) {
Weave.Svc.Obs.remove(topic, this.updateWeavePrefs, this);
}, gSyncPane);
Weave.Svc.Obs.remove(migrateTopic, gSyncPane.updateMigrationState, gSyncPane);
}, false);
this._stringBundle =
Services.strings.createBundle("chrome://browser/locale/preferences/preferences.properties");
// ask the migration module to broadcast its current state (and nothing will
// happen if it's not loaded - which is good, as that means no migration
// is pending/necessary) - we don't want to suck that module in just to
// find there's nothing to do.
Services.obs.notifyObservers(null, "fxa-migration:state-request", null);
XPCOMUtils.defineLazyGetter(this, '_stringBundle', () => {
return Services.strings.createBundle("chrome://browser/locale/preferences/preferences.properties");
}),
XPCOMUtils.defineLazyGetter(this, '_accountsStringBundle', () => {
return Services.strings.createBundle("chrome://browser/locale/accounts.properties");
}),
this.updateWeavePrefs();
},
@ -192,6 +214,17 @@ let gSyncPane = {
});
setEventListener("tosPP-small-ToS", "click", gSyncPane.openToS);
setEventListener("tosPP-small-PP", "click", gSyncPane.openPrivacyPolicy);
setEventListener("sync-migrate-upgrade", "click", function () {
let win = Services.wm.getMostRecentWindow("navigator:browser");
fxaMigrator.createFxAccount(win);
});
setEventListener("sync-migrate-forget", "click", function () {
fxaMigrator.forgetFxAccount();
});
setEventListener("sync-migrate-resend", "click", function () {
let win = Services.wm.getMostRecentWindow("navigator:browser");
fxaMigrator.resendVerificationMail(win);
});
},
updateWeavePrefs: function () {
@ -203,7 +236,6 @@ let gSyncPane = {
if (service.fxAccountsEnabled) {
// determine the fxa status...
this.page = PAGE_PLEASE_WAIT;
Components.utils.import("resource://gre/modules/FxAccounts.jsm");
fxAccounts.getSignedInUser().then(data => {
if (!data) {
this.page = FXA_PAGE_LOGGED_OUT;
@ -262,6 +294,45 @@ let gSyncPane = {
}
},
updateMigrationState: function(subject, state) {
let selIndex;
switch (state) {
case fxaMigrator.STATE_USER_FXA: {
let sb = this._accountsStringBundle;
let button = document.getElementById("sync-migrate-upgrade");
button.setAttribute("label", sb.GetStringFromName("upgradeToFxA.label"));
button.setAttribute("accesskey", sb.GetStringFromName("upgradeToFxA.accessKey"));
selIndex = 0;
break;
}
case fxaMigrator.STATE_USER_FXA_VERIFIED: {
let sb = this._accountsStringBundle;
let email = subject.QueryInterface(Components.interfaces.nsISupportsString).data;
let label = sb.formatStringFromName("needVerifiedUserLong", [email], 1);
let elt = document.getElementById("sync-migrate-verify-label");
elt.setAttribute("value", label);
// The "resend" button.
let button = document.getElementById("sync-migrate-resend");
button.setAttribute("label", sb.GetStringFromName("resendVerificationEmail.label"));
button.setAttribute("accesskey", sb.GetStringFromName("resendVerificationEmail.accessKey"));
// The "forget" button.
button = document.getElementById("sync-migrate-forget");
button.setAttribute("label", sb.GetStringFromName("forgetMigration.label"));
button.setAttribute("accesskey", sb.GetStringFromName("forgetMigration.accessKey"));
selIndex = 1;
break;
}
default:
if (state) { // |null| is expected, but everything else is not.
Cu.reportError("updateMigrationState has unknown state: " + state);
}
document.getElementById("sync-migration").hidden = true;
return;
}
document.getElementById("sync-migration").hidden = false;
document.getElementById("sync-migration-deck").selectedIndex = selIndex;
},
startOver: function (showDialog) {
if (showDialog) {
let flags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
@ -375,14 +446,13 @@ let gSyncPane = {
},
verifyFirefoxAccount: function() {
Components.utils.import("resource://gre/modules/FxAccounts.jsm");
fxAccounts.resendVerificationEmail().then(() => {
fxAccounts.getSignedInUser().then(data => {
let sb = this._stringBundle;
let title = sb.GetStringFromName("firefoxAccountsVerificationSentTitle");
let heading = sb.formatStringFromName("firefoxAccountsVerificationSentHeading",
let sb = this._accountsStringBundle;
let title = sb.GetStringFromName("verificationSentTitle");
let heading = sb.formatStringFromName("verificationSentHeading",
[data.email], 1);
let description = sb.GetStringFromName("firefoxAccountVerificationSentDescription");
let description = sb.GetStringFromName("verificationSentDescription");
let factory = Cc["@mozilla.org/prompter;1"]
.getService(Ci.nsIPromptFactory);
@ -430,7 +500,6 @@ let gSyncPane = {
return;
}
}
Cu.import("resource://gre/modules/FxAccounts.jsm");
fxAccounts.signOut().then(() => {
this.updateWeavePrefs();
});

View File

@ -37,6 +37,31 @@
<label class="header-name">&paneSync.title;</label>
</hbox>
<hbox id="sync-migration-container"
data-category="paneSync"
hidden="true">
<vbox id="sync-migration" flex="1" hidden="true">
<deck id="sync-migration-deck">
<!-- When we are in the "need FxA user" state -->
<hbox align="center">
<label>&migrate.upgradeNeeded;</label>
<spacer flex="1"/>
<button id="sync-migrate-upgrade"/>
</hbox>
<!-- When we are in the "need the user to be verified" state -->
<hbox align="center">
<label id="sync-migrate-verify-label"/>
<spacer flex="1"/>
<button id="sync-migrate-forget"/>
<button id="sync-migrate-resend"/>
</hbox>
</deck>
</vbox>
</hbox>
<deck id="weavePrefsDeck" data-category="paneSync" hidden="true">
<!-- These panels are for the "legacy" sync provider -->
<vbox id="noAccount" align="center">

View File

@ -285,11 +285,11 @@ let gSyncPane = {
Components.utils.import("resource://gre/modules/FxAccounts.jsm");
fxAccounts.resendVerificationEmail().then(() => {
fxAccounts.getSignedInUser().then(data => {
let sb = this._stringBundle;
let title = sb.GetStringFromName("firefoxAccountsVerificationSentTitle");
let heading = sb.formatStringFromName("firefoxAccountsVerificationSentHeading",
let sb = Services.strings.createBundle("chrome://browser/locale/accounts.properties");
let title = sb.GetStringFromName("verificationSentTitle");
let heading = sb.formatStringFromName("verificationSentHeading",
[data.email], 1);
let description = sb.GetStringFromName("firefoxAccountVerificationSentDescription");
let description = sb.GetStringFromName("verificationSentDescription");
Services.prompt.alert(window, title, heading + "\n\n" + description);
});

View File

@ -472,6 +472,8 @@
var textValue = textBox.value;
var where = "current";
// Open ctrl/cmd clicks on one-off buttons in a new background tab.
if (aEvent && aEvent.originalTarget.getAttribute("anonid") == "search-go-button") {
if (aEvent.button == 2)
return;
@ -479,11 +481,20 @@
}
else {
var newTabPref = textBox._prefBranch.getBoolPref("browser.search.openintab");
if ((aEvent && aEvent.altKey) ^ newTabPref)
if (((aEvent instanceof KeyboardEvent) && aEvent.altKey) ^ newTabPref)
where = "tab";
if ((aEvent instanceof MouseEvent) && (aEvent.button == 1 ||
#ifdef XP_MACOSX
aEvent.metaKey))
#else
aEvent.ctrlKey))
#endif
where = "tab-background";
}
this.doSearch(textValue, where, aEngine);
if (where == "tab-background")
this.focus();
]]></body>
</method>
@ -509,7 +520,13 @@
var submission = engine.getSubmission(aData, null, "searchbar");
BrowserSearch.recordSearchInHealthReport(engine, "searchbar");
// null parameter below specifies HTML response for search
openUILinkIn(submission.uri.spec, aWhere, null, submission.postData);
let params = {
postData: submission.postData,
inBackground: aWhere == "tab-background"
};
openUILinkIn(submission.uri.spec,
aWhere == "tab-background" ? "tab" : aWhere,
params);
]]></body>
</method>
</implementation>

View File

@ -38,7 +38,9 @@ exports.items = [{
let chromeWindow = context.environment.chromeWindow;
let target = context.environment.target;
let dropper = EyedropperManager.createInstance(chromeWindow);
let dropper = EyedropperManager.createInstance(chromeWindow,
{ context: "command",
copyOnSelect: true });
dropper.open();
eventEmitter.emit("changed", { target: target });

View File

@ -4,6 +4,7 @@
const {Cc, Ci, Cu} = require("chrome");
const {rgbToHsl} = require("devtools/css-color").colorUtils;
const Telemetry = require("devtools/shared/telemetry");
const {EventEmitter} = Cu.import("resource://gre/modules/devtools/event-emitter.js");
const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
const {setTimeout, clearTimeout} = Cu.import("resource://gre/modules/Timer.jsm", {});
@ -61,13 +62,13 @@ let EyedropperManager = {
return this._instances.get(chromeWindow);
},
createInstance: function(chromeWindow) {
createInstance: function(chromeWindow, options) {
let dropper = this.getInstance(chromeWindow);
if (dropper) {
return dropper;
}
dropper = new Eyedropper(chromeWindow);
dropper = new Eyedropper(chromeWindow, options);
this._instances.set(chromeWindow, dropper);
dropper.on("destroy", () => {
@ -100,9 +101,9 @@ exports.EyedropperManager = EyedropperManager;
* @param {DOMWindow} chromeWindow
* window to inspect
* @param {object} opts
* optional options object, with 'copyOnSelect'
* optional options object, with 'copyOnSelect', 'context'
*/
function Eyedropper(chromeWindow, opts = { copyOnSelect: true }) {
function Eyedropper(chromeWindow, opts = { copyOnSelect: true, context: "other" }) {
this.copyOnSelect = opts.copyOnSelect;
this._onFirstMouseMove = this._onFirstMouseMove.bind(this);
@ -134,6 +135,18 @@ function Eyedropper(chromeWindow, opts = { copyOnSelect: true }) {
let mm = this._contentTab.linkedBrowser.messageManager;
mm.loadFrameScript("resource:///modules/devtools/eyedropper/eyedropper-child.js", true);
// record if this was opened via the picker or standalone
var telemetry = new Telemetry();
if (opts.context == "command") {
telemetry.toolOpened("eyedropper");
}
else if (opts.context == "menu") {
telemetry.toolOpened("menueyedropper");
}
else if (opts.context == "picker") {
telemetry.toolOpened("pickereyedropper");
}
EventEmitter.decorate(this);
}

View File

@ -16,7 +16,7 @@ Cu.import("resource:///modules/devtools/ViewHelpers.jsm");
const ELLIPSIS = Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data;
const MAX_LABEL_LENGTH = 40;
let promise = require("devtools/toolkit/deprecated-sync-thenables");
let promise = require("resource://gre/modules/Promise.jsm").Promise;
const LOW_PRIORITY_ELEMENTS = {
"HEAD": true,
@ -138,13 +138,14 @@ HTMLBreadcrumbs.prototype = {
},
/**
* Print any errors (except selection guard errors).
* Warn if rejection was caused by selection change, print an error otherwise.
*/
selectionGuardEnd: function(err) {
if (err != "selection-changed") {
if (err === "selection-changed") {
console.warn("Asynchronous operation was aborted as selection changed.");
} else {
console.error(err);
}
promise.reject(err);
},
/**

View File

@ -8,7 +8,7 @@ const {Cc, Ci, Cu, Cr} = require("chrome");
Cu.import("resource://gre/modules/Services.jsm");
let promise = require("devtools/toolkit/deprecated-sync-thenables");
let promise = require("resource://gre/modules/Promise.jsm").Promise;
let EventEmitter = require("devtools/toolkit/event-emitter");
let clipboard = require("sdk/clipboard");

View File

@ -4,7 +4,7 @@
"use strict";
const promise = require("devtools/toolkit/deprecated-sync-thenables");
const promise = require("resource://gre/modules/Promise.jsm").Promise;
loader.lazyGetter(this, "EventEmitter", () => require("devtools/toolkit/event-emitter"));
loader.lazyGetter(this, "AutocompletePopup", () => require("devtools/shared/autocomplete-popup").AutocompletePopup);

View File

@ -170,6 +170,18 @@ Telemetry.prototype = {
userHistogram: "DEVTOOLS_RESPONSIVE_OPENED_PER_USER_FLAG",
timerHistogram: "DEVTOOLS_RESPONSIVE_TIME_ACTIVE_SECONDS"
},
eyedropper: {
histogram: "DEVTOOLS_EYEDROPPER_OPENED_BOOLEAN",
userHistogram: "DEVTOOLS_EYEDROPPER_OPENED_PER_USER_FLAG",
},
menueyedropper: {
histogram: "DEVTOOLS_MENU_EYEDROPPER_OPENED_BOOLEAN",
userHistogram: "DEVTOOLS_MENU_EYEDROPPER_OPENED_PER_USER_FLAG",
},
pickereyedropper: {
histogram: "DEVTOOLS_PICKER_EYEDROPPER_OPENED_BOOLEAN",
userHistogram: "DEVTOOLS_PICKER_EYEDROPPER_OPENED_PER_USER_FLAG",
},
developertoolbar: {
histogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_BOOLEAN",
userHistogram: "DEVTOOLS_DEVELOPERTOOLBAR_OPENED_PER_USER_FLAG",

View File

@ -50,6 +50,7 @@ support-files =
[browser_tableWidget_keyboard_interaction.js]
[browser_tableWidget_mouse_interaction.js]
skip-if = buildapp == 'mulet'
[browser_telemetry_button_eyedropper.js]
[browser_telemetry_button_paintflashing.js]
[browser_telemetry_button_responsive.js]
[browser_telemetry_button_scratchpad.js]

View File

@ -0,0 +1,110 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_button_eyedropper.js</p><div>test</div>";
let promise = Cu.import("resource://gre/modules/devtools/deprecated-sync-thenables.js", {}).Promise;
let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
let Telemetry = require("devtools/shared/telemetry");
let { EyedropperManager } = require("devtools/eyedropper/eyedropper");
function init() {
Telemetry.prototype.telemetryInfo = {};
Telemetry.prototype._oldlog = Telemetry.prototype.log;
Telemetry.prototype.log = function(histogramId, value) {
if (!this.telemetryInfo) {
// Can be removed when Bug 992911 lands (see Bug 1011652 Comment 10)
return;
}
if (histogramId) {
if (!this.telemetryInfo[histogramId]) {
this.telemetryInfo[histogramId] = [];
}
this.telemetryInfo[histogramId].push(value);
}
};
testButton("command-button-eyedropper");
}
function testButton(id) {
info("Testing " + id);
let target = TargetFactory.forTab(gBrowser.selectedTab);
gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
info("inspector opened");
let button = toolbox.doc.querySelector("#" + id);
ok(button, "Captain, we have the button");
// open the eyedropper
button.click();
checkResults("_EYEDROPPER_");
}).then(null, console.error);
}
function clickButton(node, clicks) {
for (let i = 0; i < clicks; i++) {
info("Clicking button " + node.id);
node.click();
}
}
function checkResults(histIdFocus) {
let result = Telemetry.prototype.telemetryInfo;
for (let [histId, value] of Iterator(result)) {
if (histId.startsWith("DEVTOOLS_INSPECTOR_") ||
!histId.contains(histIdFocus)) {
// Inspector stats are tested in
// browser_telemetry_toolboxtabs_{toolname}.js so we skip them here
// because we only open the inspector once for this test.
continue;
}
if (histId.endsWith("OPENED_PER_USER_FLAG")) {
ok(value.length === 1 && value[0] === true,
"Per user value " + histId + " has a single value of true");
} else if (histId.endsWith("OPENED_BOOLEAN")) {
ok(value.length == 1, histId + " has one entry");
let okay = value.every(function(element) {
return element === true;
});
ok(okay, "All " + histId + " entries are === true");
}
}
finishUp();
}
function finishUp() {
gBrowser.removeCurrentTab();
Telemetry.prototype.log = Telemetry.prototype._oldlog;
delete Telemetry.prototype._oldlog;
delete Telemetry.prototype.telemetryInfo;
TargetFactory = Services = promise = require = null;
finish();
}
function test() {
waitForExplicitFinish();
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", function() {
gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
waitForFocus(init, content);
}, true);
content.location = TEST_URI;
}

View File

@ -1086,7 +1086,8 @@ SwatchColorPickerTooltip.prototype = Heritage.extend(SwatchBasedEditorTooltip.pr
chromeWindow = Services.wm.getMostRecentWindow("navigator:browser");
chromeWindow.focus();
}
let dropper = new Eyedropper(chromeWindow, { copyOnSelect: false });
let dropper = new Eyedropper(chromeWindow, { copyOnSelect: false,
context: "picker" });
dropper.once("select", (event, color) => {
if (toolboxWindow) {

View File

@ -4,6 +4,19 @@
"use strict";
// So we can test collecting telemetry on the eyedropper
let oldCanRecord = Services.telemetry.canRecord;
Services.telemetry.canRecord = true;
registerCleanupFunction(function () {
Services.telemetry.canRecord = oldCanRecord;
});
const HISTOGRAM_ID = "DEVTOOLS_PICKER_EYEDROPPER_OPENED_BOOLEAN";
const FLAG_HISTOGRAM_ID = "DEVTOOLS_PICKER_EYEDROPPER_OPENED_PER_USER_FLAG";
const EXPECTED_TELEMETRY = {
"DEVTOOLS_PICKER_EYEDROPPER_OPENED_BOOLEAN": 2,
"DEVTOOLS_PICKER_EYEDROPPER_OPENED_PER_USER_FLAG": 1
}
const PAGE_CONTENT = [
'<style type="text/css">',
' body {',
@ -34,6 +47,9 @@ const EXPECTED_COLOR = "rgb(255, 255, 85)"; // #ff5
// to close it, and clicking the page to select a color.
add_task(function*() {
// clear telemetry so we can get accurate counts
clearTelemetry();
yield addTab("data:text/html;charset=utf-8,rule view eyedropper test");
content.document.body.innerHTML = PAGE_CONTENT;
@ -57,6 +73,8 @@ add_task(function*() {
ok(dropper, "dropper opened");
yield testSelect(swatch, dropper);
checkTelemetry();
});
function testESC(swatch, dropper) {
@ -97,6 +115,23 @@ function testSelect(swatch, dropper) {
return deferred.promise;
}
function clearTelemetry() {
for (let histogramId in EXPECTED_TELEMETRY) {
let histogram = Services.telemetry.getHistogramById(histogramId);
histogram.clear();
}
}
function checkTelemetry() {
for (let histogramId in EXPECTED_TELEMETRY) {
let expected = EXPECTED_TELEMETRY[histogramId];
let histogram = Services.telemetry.getHistogramById(histogramId);
let snapshot = histogram.snapshot();
is (snapshot.counts[1], expected,
"eyedropper telemetry value correct for " + histogramId);
}
}
/* Helpers */

View File

@ -28,12 +28,20 @@ function spawnTest() {
"curve": "Float32Array"
}, "WaveShaper's `curve` is listed as an `Float32Array`.");
let aVar = gVars.getScopeAtIndex(0).get("curve")
let state = aVar.target.querySelector(".theme-twisty").hasAttribute("invisible");
ok(state, "Float32Array property should not have a dropdown.");
click(panelWin, findGraphNode(panelWin, nodeIds[1]));
yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
checkVariableView(gVars, 0, {
"buffer": "AudioBuffer"
}, "AudioBufferSourceNode's `buffer` is listed as an `AudioBuffer`.");
aVar = gVars.getScopeAtIndex(0).get("buffer")
state = aVar.target.querySelector(".theme-twisty").hasAttribute("invisible");
ok(state, "AudioBuffer property should not have a dropdown.");
yield teardown(panel);
finish();
}

View File

@ -150,7 +150,10 @@ let InspectorView = {
value: value,
writable: !flags || !flags.readonly,
};
audioParamsScope.addItem(param, descriptor);
let item = audioParamsScope.addItem(param, descriptor);
// No items should currently display a dropdown
item.twisty = false;
});
audioParamsScope.expanded = true;

View File

@ -17,3 +17,17 @@ needVerifiedUserLong = Please click the verification link in the email sent to %
resendVerificationEmail.label = Resend
resendVerificationEmail.accessKey = R
forgetMigration.label = Forget
forgetMigration.accessKey = F
# These strings are used in a dialog we display after the user requests we resend
# a verification email.
verificationSentTitle = Verification Sent
# LOCALIZATION NOTE (verificationSentHeading) - %S = Email address of user's Firefox Account
verificationSentHeading = A verification link has been sent to %S
verificationSentDescription = Please check your email and click the link to begin syncing.
verificationNotSentTitle = Unable to Send Verification
verificationNotSentHeading = We are unable to send a verification mail at this time
verificationNotSentDescription = Please try again later.

View File

@ -137,9 +137,3 @@ syncUnlinkConfirm.label=Unlink
featureEnableRequiresRestart=%S must restart to enable this feature.
featureDisableRequiresRestart=%S must restart to disable this feature.
shouldRestartTitle=Restart %S
###Preferences::Sync::Firefox Accounts
firefoxAccountsVerificationSentTitle=Verification Sent
# LOCALIZATION NOTE: %S = user's email address.
firefoxAccountsVerificationSentHeading=A verification link has been sent to %S
firefoxAccountVerificationSentDescription=Please check your email and click the link to begin syncing.

View File

@ -78,3 +78,6 @@ both, to better adapt this sentence to their language.
<!ENTITY welcome.createAccount.label "Create Account">
<!ENTITY welcome.useOldSync.label "Using an older version of Sync?">
<!-- Sync Migration -->
<!ENTITY migrate.upgradeNeeded "The sync account system is being discontinued. A new Firefox Account is required to sync.">

View File

@ -543,6 +543,16 @@ this.UITour = {
break;
}
case "setConfiguration": {
if (typeof data.configuration != "string") {
log.warn("setConfiguration: No configuration option specified");
return false;
}
this.setConfiguration(data.configuration, data.value);
break;
}
case "showFirefoxAccounts": {
// 'signup' is the only action that makes sense currently, so we don't
// accept arbitrary actions just to be safe...
@ -1321,7 +1331,7 @@ this.UITour = {
// An event object is expected but we don't want to toggle the panel with a click if the panel
// is already open.
aWindow.LoopUI.openCallPanel({ target: toolbarButton.node, }).then(() => {
aWindow.LoopUI.openCallPanel({ target: toolbarButton.node, }, "rooms").then(() => {
if (aOpenCallback) {
aOpenCallback();
}
@ -1479,6 +1489,18 @@ this.UITour = {
}
},
setConfiguration: function(aConfiguration, aValue) {
switch (aConfiguration) {
case "Loop:ResumeTourOnFirstJoin":
// Ignore aValue in this case to avoid accidentally setting it to false.
Services.prefs.setBoolPref("loop.gettingStarted.resumeOnFirstJoin", true);
break;
default:
log.error("setConfiguration: Unknown configuration requested: " + aConfiguration);
break;
}
},
getAvailableTargets: function(aMessageManager, aChromeWindow, aCallbackID) {
Task.spawn(function*() {
let window = aChromeWindow;

View File

@ -173,6 +173,68 @@ let tests = [
yield showInfoPromise(currentTarget, "This is " + currentTarget, "My arrow should be underneath");
is(popup.popupBoxObject.alignmentPosition, "after_end", "Check " + currentTarget + " position");
}),
taskify(function* test_setConfiguration() {
is(Services.prefs.getBoolPref("loop.gettingStarted.resumeOnFirstJoin"), false, "pref should be false but exist");
gContentAPI.setConfiguration("Loop:ResumeTourOnFirstJoin", true);
yield waitForConditionPromise(() => {
return Services.prefs.getBoolPref("loop.gettingStarted.resumeOnFirstJoin");
}, "Pref should change to true via setConfiguration");
Services.prefs.clearUserPref("loop.gettingStarted.resumeOnFirstJoin");
}),
taskify(function* test_resumeViaMenuPanel_roomClosedTabOpen() {
Services.prefs.setBoolPref("loop.gettingStarted.resumeOnFirstJoin", true);
// Create a fake room and then add a fake non-owner participant
let roomsMap = setupFakeRoom();
roomsMap.get("fakeTourRoom").participants = [{
owner: false,
}];
// Set the tour URL to be the current page with a different query param
let gettingStartedURL = gTestTab.linkedBrowser.currentURI.resolve("?gettingstarted=1");
Services.prefs.setCharPref("loop.gettingStarted.url", gettingStartedURL);
let observationPromise = new Promise((resolve) => {
gContentAPI.observe((event, params) => {
is(event, "Loop:IncomingConversation", "Page should have been notified about incoming conversation");
ise(params.conversationOpen, false, "conversationOpen should be false");
is(gBrowser.selectedTab, gTestTab, "The same tab should be selected");
resolve();
});
});
// Now open the menu while that non-owner is in the fake room to trigger resuming the tour
yield showMenuPromise("loop");
yield observationPromise;
Services.prefs.clearUserPref("loop.gettingStarted.resumeOnFirstJoin");
}),
taskify(function* test_resumeViaMenuPanel_roomClosedTabClosed() {
Services.prefs.setBoolPref("loop.gettingStarted.resumeOnFirstJoin", true);
// Create a fake room and then add a fake non-owner participant
let roomsMap = setupFakeRoom();
roomsMap.get("fakeTourRoom").participants = [{
owner: false,
}];
// Set the tour URL to a page that's not open yet
Services.prefs.setCharPref("loop.gettingStarted.url", gBrowser.currentURI.prePath);
let newTabPromise = waitForConditionPromise(() => {
return gBrowser.currentURI.path.contains("incomingConversation=waiting");
}, "New tab with incomingConversation=waiting should have opened");
// Now open the menu while that non-owner is in the fake room to trigger resuming the tour
yield showMenuPromise("loop");
yield newTabPromise;
yield gBrowser.removeCurrentTab();
Services.prefs.clearUserPref("loop.gettingStarted.resumeOnFirstJoin");
}),
];
// End tests
@ -188,9 +250,11 @@ function setupFakeRoom() {
let room = {};
for (let prop of ["roomToken", "roomName", "roomOwner", "roomUrl", "participants"])
room[prop] = "fakeTourRoom";
LoopRooms.stubCache(new Map([
let roomsMap = new Map([
[room.roomToken, room]
]));
]);
LoopRooms.stubCache(roomsMap);
return roomsMap;
}
if (Services.prefs.getBoolPref("loop.enabled")) {
@ -201,7 +265,9 @@ if (Services.prefs.getBoolPref("loop.enabled")) {
Services.prefs.setCharPref("services.push.serverURL", "ws://localhost/");
registerCleanupFunction(() => {
Services.prefs.clearUserPref("loop.gettingStarted.resumeOnFirstJoin");
Services.prefs.clearUserPref("loop.gettingStarted.seen");
Services.prefs.clearUserPref("loop.gettingStarted.url");
Services.prefs.clearUserPref("loop.server");
Services.prefs.clearUserPref("services.push.serverURL");
@ -214,6 +280,9 @@ if (Services.prefs.getBoolPref("loop.enabled")) {
if (frame) {
frame.remove();
}
// Remove the stubbed rooms
LoopRooms.stubCache(null);
});
} else {
ok(true, "Loop is disabled so skip the UITour Loop tests");

View File

@ -223,6 +223,13 @@ if (typeof Mozilla == 'undefined') {
});
};
Mozilla.UITour.setConfiguration = function(configName, configValue) {
_sendEvent('setConfiguration', {
configuration: configName,
value: configValue,
});
};
Mozilla.UITour.showFirefoxAccounts = function() {
_sendEvent('showFirefoxAccounts');
};

View File

@ -87,6 +87,18 @@ label.small {
-moz-box-flex: 1;
}
/* Privacy Pane */
/* styles for the link elements copied from .text-link in global.css */
.inline-link {
color: -moz-nativehyperlinktext;
text-decoration: none;
}
.inline-link:hover {
text-decoration: underline;
}
/* Modeless Window Dialogs */
.windowDialog,
.windowDialog prefpane {

View File

@ -184,6 +184,24 @@ caption {
font-weight: bold;
}
/**
* Privacy Pane
*/
html|a.inline-link {
color: -moz-nativehyperlinktext;
text-decoration: none;
}
html|a.inline-link:hover {
text-decoration: underline;
}
html|a.inline-link:-moz-focusring {
outline-width: 0;
box-shadow: @focusRingShadow@;
}
/**
* Update Preferences
*/

View File

@ -173,16 +173,6 @@ description > html|a {
cursor: pointer;
}
#offlineAppsList,
#syncEnginesList {
-moz-appearance: none;
color: #333333;
padding: 10px;
border: 1px solid #C1C1C1;
border-radius: 2px;
background-color: #FBFBFB;
}
#noFxaAccount {
/* Overriding the margins from the base preferences.css theme file.
These overrides can be simplified by fixing bug 1027174 */
@ -293,3 +283,20 @@ description > html|a {
/**
* End Dialog
*/
/**
* Sync migration
*/
#sync-migration {
border: 1px solid rgba(0, 0, 0, 0.32);
background-color: InfoBackground;
color: InfoText;
text-shadow: none;
margin: 5px 0 0 0;
animation: fadein 3000ms;
}
@keyframes fadein {
from { opacity: 0; }
to { opacity: 1; }
}

View File

@ -83,6 +83,18 @@ label.small {
-moz-box-flex: 1;
}
/* Privacy Pane */
/* styles for the link elements copied from .text-link in global.css */
.inline-link {
color: -moz-nativehyperlinktext;
text-decoration: none;
}
.inline-link:hover {
text-decoration: underline;
}
/* Modeless Window Dialogs */
.windowDialog,
.windowDialog prefpane {

View File

@ -3856,7 +3856,6 @@ if test -n "$MOZ_FMP4"; then
else
MOZ_FMP4=
fi
MOZ_EME=1
MOZ_FFMPEG=
MOZ_WEBRTC=1
MOZ_PEERCONNECTION=
@ -5327,6 +5326,7 @@ MOZ_ARG_DISABLE_BOOL(fmp4,
if test -n "$MOZ_FMP4"; then
AC_DEFINE(MOZ_FMP4)
MOZ_EME=1
fi;
dnl ========================================================
@ -5339,6 +5339,9 @@ MOZ_ARG_DISABLE_BOOL(eme,
MOZ_EME=1)
if test -n "$MOZ_EME"; then
if test -z "$MOZ_FMP4"; then
AC_MSG_ERROR([Encrypted Media Extension support requires Fragmented MP4 support])
fi
AC_DEFINE(MOZ_EME)
fi;

View File

@ -16,6 +16,7 @@ let URL = '<!DOCTYPE html><style>' +
let TESTS = [{
desc: "Changing the width of the test element",
searchFor: "Paint",
setup: function(div) {
div.setAttribute("class", "resize-change-color");
},
@ -35,6 +36,7 @@ let TESTS = [{
}
}, {
desc: "Changing the test element's background color",
searchFor: "Paint",
setup: function(div) {
div.setAttribute("class", "change-color");
},
@ -52,6 +54,7 @@ let TESTS = [{
}
}, {
desc: "Changing the test element's classname",
searchFor: "Paint",
setup: function(div) {
div.setAttribute("class", "change-color add-class");
},
@ -63,6 +66,7 @@ let TESTS = [{
}
}, {
desc: "sync console.time/timeEnd",
searchFor: "ConsoleTime",
setup: function(div, docShell) {
content.console.time("FOOBAR");
content.console.timeEnd("FOOBAR");
@ -102,7 +106,7 @@ let test = Task.async(function*() {
info("Start recording");
docShell.recordProfileTimelineMarkers = true;
for (let {desc, setup, check} of TESTS) {
for (let {desc, searchFor, setup, check} of TESTS) {
info("Running test: " + desc);
@ -110,7 +114,7 @@ let test = Task.async(function*() {
docShell.popProfileTimelineMarkers();
info("Running the test setup function");
let onMarkers = waitForMarkers(docShell);
let onMarkers = waitForMarkers(docShell, searchFor);
setup(div, docShell);
info("Waiting for new markers on the docShell");
let markers = yield onMarkers;
@ -140,21 +144,20 @@ function openUrl(url) {
});
}
function waitForMarkers(docshell) {
function waitForMarkers(docshell, searchFor) {
return new Promise(function(resolve, reject) {
let waitIterationCount = 0;
let maxWaitIterationCount = 10; // Wait for 2sec maximum
let markers = [];
let interval = setInterval(() => {
let markers = docshell.popProfileTimelineMarkers();
if (markers.length > 0) {
let newMarkers = docshell.popProfileTimelineMarkers();
markers = [...markers, ...newMarkers];
if (newMarkers.some(m => m.name == searchFor) ||
waitIterationCount > maxWaitIterationCount) {
clearInterval(interval);
resolve(markers);
}
if (waitIterationCount > maxWaitIterationCount) {
clearInterval(interval);
resolve([]);
}
waitIterationCount++;
}, 200);
});

View File

@ -20,10 +20,11 @@ using namespace mozilla;
CameraControlImpl::CameraControlImpl()
: mListenerLock(PR_NewRWLock(PR_RWLOCK_RANK_NONE, "CameraControlImpl.Listeners.Lock"))
, mPreviewState(CameraControlListener::kPreviewStopped)
, mHardwareState(CameraControlListener::kHardwareClosed)
, mHardwareState(CameraControlListener::kHardwareUninitialized)
, mHardwareStateChangeReason(NS_OK)
{
DOM_CAMERA_LOGT("%s:%d : this=%p\n", __func__, __LINE__, this);
mCurrentConfiguration.mMode = ICameraControl::kUnspecifiedMode;
// reuse the same camera thread to conserve resources
nsCOMPtr<nsIThread> ct = do_QueryInterface(sCameraThread);
@ -79,7 +80,7 @@ CameraControlImpl::OnHardwareStateChange(CameraControlListener::HardwareState aN
}
#ifdef PR_LOGGING
const char* state[] = { "closed", "open", "failed" };
const char* state[] = { "uninitialized", "closed", "open", "failed" };
MOZ_ASSERT(aNewState >= 0);
if (static_cast<unsigned int>(aNewState) < sizeof(state) / sizeof(state[0])) {
DOM_CAMERA_LOGI("New hardware state is '%s' (reason=0x%x)\n",

View File

@ -34,6 +34,7 @@ public:
enum HardwareState
{
kHardwareUninitialized,
kHardwareClosed,
kHardwareOpen,
kHardwareOpenFailed

View File

@ -202,6 +202,7 @@ nsDOMCameraControl::nsDOMCameraControl(uint32_t aCameraId,
, mGetCameraPromise(aPromise)
, mWindow(aWindow)
, mPreviewState(CameraControlListener::kPreviewStopped)
, mSetInitialConfig(false)
{
DOM_CAMERA_LOGT("%s:%d : this=%p\n", __func__, __LINE__, this);
mInput = new CameraPreviewMediaStream(this);
@ -236,8 +237,14 @@ nsDOMCameraControl::nsDOMCameraControl(uint32_t aCameraId,
}
if (haveInitialConfig) {
config.mPreviewSize.width = aInitialConfig.mPreviewSize.mWidth;
config.mPreviewSize.height = aInitialConfig.mPreviewSize.mHeight;
rv = SelectPreviewSize(aInitialConfig.mPreviewSize, config.mPreviewSize);
if (NS_FAILED(rv)) {
mListener->OnUserError(DOMCameraControlListener::kInStartCamera, rv);
return;
}
config.mPictureSize.width = aInitialConfig.mPictureSize.mWidth;
config.mPictureSize.height = aInitialConfig.mPictureSize.mHeight;
config.mRecorderProfile = aInitialConfig.mRecorderProfile;
}
@ -274,6 +281,9 @@ nsDOMCameraControl::nsDOMCameraControl(uint32_t aCameraId,
// Start the camera...
if (haveInitialConfig) {
rv = mCameraControl->Start(&config);
if (NS_SUCCEEDED(rv)) {
mSetInitialConfig = true;
}
} else {
rv = mCameraControl->Start();
}
@ -281,6 +291,9 @@ nsDOMCameraControl::nsDOMCameraControl(uint32_t aCameraId,
} else {
if (haveInitialConfig) {
rv = mCameraControl->SetConfiguration(config);
if (NS_SUCCEEDED(rv)) {
mSetInitialConfig = true;
}
} else {
rv = NS_OK;
}
@ -308,6 +321,46 @@ nsDOMCameraControl::IsWindowStillActive()
return nsDOMCameraManager::IsWindowStillActive(mWindow->WindowID());
}
nsresult
nsDOMCameraControl::SelectPreviewSize(const CameraSize& aRequestedPreviewSize, ICameraControl::Size& aSelectedPreviewSize)
{
if (aRequestedPreviewSize.mWidth && aRequestedPreviewSize.mHeight) {
aSelectedPreviewSize.width = aRequestedPreviewSize.mWidth;
aSelectedPreviewSize.height = aRequestedPreviewSize.mHeight;
} else {
/* Use the window width and height if no preview size is provided.
Note that the width and height are actually reversed from the
camera perspective. */
int32_t width = 0;
int32_t height = 0;
float ratio = 0.0;
nsresult rv;
rv = mWindow->GetDevicePixelRatio(&ratio);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = mWindow->GetInnerWidth(&height);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = mWindow->GetInnerHeight(&width);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
MOZ_ASSERT(width > 0);
MOZ_ASSERT(height > 0);
MOZ_ASSERT(ratio > 0.0);
aSelectedPreviewSize.width = std::ceil(width * ratio);
aSelectedPreviewSize.height = std::ceil(height * ratio);
}
return NS_OK;
}
// Setter for weighted regions: { top, bottom, left, right, weight }
nsresult
nsDOMCameraControl::Set(uint32_t aKey, const Optional<Sequence<CameraRegion> >& aValue, uint32_t aLimit)
@ -801,9 +854,14 @@ nsDOMCameraControl::SetConfiguration(const CameraConfiguration& aConfiguration,
}
ICameraControl::Configuration config;
aRv = SelectPreviewSize(aConfiguration.mPreviewSize, config.mPreviewSize);
if (aRv.Failed()) {
return nullptr;
}
config.mRecorderProfile = aConfiguration.mRecorderProfile;
config.mPreviewSize.width = aConfiguration.mPreviewSize.mWidth;
config.mPreviewSize.height = aConfiguration.mPreviewSize.mHeight;
config.mPictureSize.width = aConfiguration.mPictureSize.mWidth;
config.mPictureSize.height = aConfiguration.mPictureSize.mHeight;
config.mMode = ICameraControl::kPictureMode;
if (aConfiguration.mMode == CameraMode::Video) {
config.mMode = ICameraControl::kVideoMode;
@ -1041,6 +1099,20 @@ nsDOMCameraControl::DispatchStateEvent(const nsString& aType, const nsString& aS
DispatchTrustedEvent(event);
}
void
nsDOMCameraControl::OnGetCameraComplete()
{
// The hardware is open, so we can return a camera to JS, even if
// the preview hasn't started yet.
nsRefPtr<Promise> promise = mGetCameraPromise.forget();
if (promise) {
CameraGetPromiseData data;
data.mCamera = this;
data.mConfiguration = *mCurrentConfiguration;
promise->MaybeResolve(data);
}
}
// Camera Control event handlers--must only be called from the Main Thread!
void
nsDOMCameraControl::OnHardwareStateChange(CameraControlListener::HardwareState aState,
@ -1055,22 +1127,16 @@ nsDOMCameraControl::OnHardwareStateChange(CameraControlListener::HardwareState a
case CameraControlListener::kHardwareOpen:
DOM_CAMERA_LOGI("DOM OnHardwareStateChange: open\n");
MOZ_ASSERT(aReason == NS_OK);
{
if (!mSetInitialConfig) {
// The hardware is open, so we can return a camera to JS, even if
// the preview hasn't started yet.
nsRefPtr<Promise> promise = mGetCameraPromise.forget();
if (promise) {
CameraGetPromiseData data;
data.mCamera = this;
data.mConfiguration = *mCurrentConfiguration;
promise->MaybeResolve(data);
}
OnGetCameraComplete();
}
break;
case CameraControlListener::kHardwareClosed:
DOM_CAMERA_LOGI("DOM OnHardwareStateChange: closed\n");
{
if (!mSetInitialConfig) {
nsRefPtr<Promise> promise = mReleasePromise.forget();
if (promise) {
promise->MaybeResolve(JS::UndefinedHandleValue);
@ -1102,6 +1168,9 @@ nsDOMCameraControl::OnHardwareStateChange(CameraControlListener::HardwareState a
NS_LITERAL_STRING("close"),
eventInit);
DispatchTrustedEvent(event);
} else {
// The configuration failed and we forced the camera to shutdown.
OnUserError(DOMCameraControlListener::kInStartCamera, NS_ERROR_NOT_AVAILABLE);
}
break;
@ -1111,6 +1180,9 @@ nsDOMCameraControl::OnHardwareStateChange(CameraControlListener::HardwareState a
OnUserError(DOMCameraControlListener::kInStartCamera, NS_ERROR_NOT_AVAILABLE);
break;
case CameraControlListener::kHardwareUninitialized:
break;
default:
DOM_CAMERA_LOGE("DOM OnHardwareStateChange: UNKNOWN=%d\n", aState);
MOZ_ASSERT_UNREACHABLE("Unanticipated camera hardware state");
@ -1227,9 +1299,17 @@ nsDOMCameraControl::OnConfigurationChange(DOMCameraConfiguration* aConfiguration
mCurrentConfiguration->mMaxMeteringAreas);
DOM_CAMERA_LOGI(" preview size (w x h) : %d x %d\n",
mCurrentConfiguration->mPreviewSize.mWidth, mCurrentConfiguration->mPreviewSize.mHeight);
DOM_CAMERA_LOGI(" picture size (w x h) : %d x %d\n",
mCurrentConfiguration->mPictureSize.mWidth, mCurrentConfiguration->mPictureSize.mHeight);
DOM_CAMERA_LOGI(" recorder profile : %s\n",
NS_ConvertUTF16toUTF8(mCurrentConfiguration->mRecorderProfile).get());
if (mSetInitialConfig) {
OnGetCameraComplete();
mSetInitialConfig = false;
return;
}
nsRefPtr<Promise> promise = mSetConfigurationPromise.forget();
if (promise) {
promise->MaybeResolve(*aConfiguration);
@ -1241,6 +1321,9 @@ nsDOMCameraControl::OnConfigurationChange(DOMCameraConfiguration* aConfiguration
eventInit.mPreviewSize = new DOMRect(static_cast<DOMMediaStream*>(this), 0, 0,
mCurrentConfiguration->mPreviewSize.mWidth,
mCurrentConfiguration->mPreviewSize.mHeight);
eventInit.mPictureSize = new DOMRect(static_cast<DOMMediaStream*>(this), 0, 0,
mCurrentConfiguration->mPictureSize.mWidth,
mCurrentConfiguration->mPictureSize.mHeight);
nsRefPtr<CameraConfigurationEvent> event =
CameraConfigurationEvent::Constructor(this,
@ -1359,6 +1442,16 @@ nsDOMCameraControl::OnUserError(CameraControlListener::UserContext aContext, nsr
break;
case CameraControlListener::kInSetConfiguration:
if (mSetInitialConfig) {
// If the SetConfiguration() call in the constructor fails, there
// is nothing we can do except release the camera hardware. This
// will trigger a hardware state change, and when the flag that
// got us here is set in that handler, we replace the normal reason
// code with one that indicates the hardware isn't available.
DOM_CAMERA_LOGI("Failed to configure cached camera, stopping\n");
mCameraControl->Stop();
return;
}
promise = mSetConfigurationPromise.forget();
break;

View File

@ -171,6 +171,7 @@ protected:
void OnTakePictureComplete(nsIDOMBlob* aPicture);
void OnFacesDetected(const nsTArray<ICameraControl::Face>& aFaces);
void OnGetCameraComplete();
void OnHardwareStateChange(DOMCameraControlListener::HardwareState aState, nsresult aReason);
void OnPreviewStateChange(DOMCameraControlListener::PreviewState aState);
void OnRecorderStateChange(CameraControlListener::RecorderState aState, int32_t aStatus, int32_t aTrackNum);
@ -179,6 +180,7 @@ protected:
void OnUserError(CameraControlListener::UserContext aContext, nsresult aError);
bool IsWindowStillActive();
nsresult SelectPreviewSize(const dom::CameraSize& aRequestedPreviewSize, ICameraControl::Size& aSelectedPreviewSize);
nsresult NotifyRecordingStatusChange(const nsString& aMsg);
@ -222,6 +224,8 @@ protected:
nsRefPtr<DeviceStorageFileDescriptor> mDSFileDescriptor;
DOMCameraControlListener::PreviewState mPreviewState;
bool mSetInitialConfig;
#ifdef MOZ_WIDGET_GONK
// cached camera control, to improve start-up time
static StaticRefPtr<ICameraControl> sCachedCameraControl;

View File

@ -215,6 +215,8 @@ DOMCameraControlListener::OnConfigurationChange(const CameraListenerConfiguratio
config->mRecorderProfile = mConfiguration.mRecorderProfile;
config->mPreviewSize.mWidth = mConfiguration.mPreviewSize.width;
config->mPreviewSize.mHeight = mConfiguration.mPreviewSize.height;
config->mPictureSize.mWidth = mConfiguration.mPictureSize.width;
config->mPictureSize.mHeight = mConfiguration.mPictureSize.height;
config->mMaxMeteringAreas = mConfiguration.mMaxMeteringAreas;
config->mMaxFocusAreas = mConfiguration.mMaxFocusAreas;

View File

@ -62,7 +62,6 @@ using namespace android;
// Construct nsGonkCameraControl on the main thread.
nsGonkCameraControl::nsGonkCameraControl(uint32_t aCameraId)
: mCameraId(aCameraId)
, mLastPictureSize({0, 0})
, mLastThumbnailSize({0, 0})
, mPreviewFps(30)
, mResumePreviewAfterTakingPicture(false) // XXXmikeh - see bug 950102
@ -136,10 +135,16 @@ nsGonkCameraControl::StartInternal(const Configuration* aInitialConfig)
}
OnHardwareStateChange(CameraControlListener::kHardwareOpen, NS_OK);
if (aInitialConfig) {
return StartPreviewImpl();
}
if (aInitialConfig) {
rv = StartPreviewInternal();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
OnConfigurationChange();
OnPreviewStateChange(CameraControlListener::kPreviewStarted);
}
return NS_OK;
}
@ -180,7 +185,7 @@ nsGonkCameraControl::Initialize()
mParams.Get(CAMERA_PARAM_SUPPORTED_MAXFOCUSAREAS, areas);
mCurrentConfiguration.mMaxFocusAreas = areas != -1 ? areas : 0;
mParams.Get(CAMERA_PARAM_PICTURE_SIZE, mLastPictureSize);
mParams.Get(CAMERA_PARAM_PICTURE_SIZE, mCurrentConfiguration.mPictureSize);
mParams.Get(CAMERA_PARAM_PREVIEWSIZE, mCurrentConfiguration.mPreviewSize);
nsString luminance; // check for support
@ -197,7 +202,7 @@ nsGonkCameraControl::Initialize()
DOM_CAMERA_LOGI(" - maximum metering areas: %u\n", mCurrentConfiguration.mMaxMeteringAreas);
DOM_CAMERA_LOGI(" - maximum focus areas: %u\n", mCurrentConfiguration.mMaxFocusAreas);
DOM_CAMERA_LOGI(" - default picture size: %u x %u\n",
mLastPictureSize.width, mLastPictureSize.height);
mCurrentConfiguration.mPictureSize.width, mCurrentConfiguration.mPictureSize.height);
DOM_CAMERA_LOGI(" - default picture file format: %s\n",
NS_ConvertUTF16toUTF8(mFileFormat).get());
DOM_CAMERA_LOGI(" - default picture quality: %f\n", quality);
@ -222,6 +227,11 @@ nsGonkCameraControl::Initialize()
mParams.Get(CAMERA_PARAM_VIDEOSIZE, mLastRecorderSize);
DOM_CAMERA_LOGI(" - default video recorder size: %u x %u\n",
mLastRecorderSize.width, mLastRecorderSize.height);
Size preferred;
mParams.Get(CAMERA_PARAM_PREFERRED_PREVIEWSIZE_FOR_VIDEO, preferred);
DOM_CAMERA_LOGI(" - preferred video preview size: %u x %u\n",
preferred.width, preferred.height);
} else {
mLastRecorderSize = mCurrentConfiguration.mPreviewSize;
}
@ -261,23 +271,66 @@ nsGonkCameraControl::~nsGonkCameraControl()
DOM_CAMERA_LOGT("%s:%d\n", __func__, __LINE__);
}
nsresult
nsGonkCameraControl::ValidateConfiguration(const Configuration& aConfig, Configuration& aValidatedConfig)
{
nsAutoTArray<Size, 16> supportedSizes;
Get(CAMERA_PARAM_SUPPORTED_PICTURESIZES, supportedSizes);
nsresult rv = GetSupportedSize(aConfig.mPictureSize, supportedSizes,
aValidatedConfig.mPictureSize);
if (NS_FAILED(rv)) {
DOM_CAMERA_LOGW("Unable to find a picture size close to %ux%u\n",
aConfig.mPictureSize.width, aConfig.mPictureSize.height);
return NS_ERROR_INVALID_ARG;
}
rv = LoadRecorderProfiles();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
nsString profileName = aConfig.mRecorderProfile;
if (profileName.IsEmpty()) {
profileName.AssignASCII("default");
}
RecorderProfile* profile;
if (!mRecorderProfiles.Get(profileName, &profile)) {
DOM_CAMERA_LOGE("Recorder profile '%s' is not supported\n",
NS_ConvertUTF16toUTF8(aConfig.mRecorderProfile).get());
return NS_ERROR_INVALID_ARG;
}
aValidatedConfig.mMode = aConfig.mMode;
aValidatedConfig.mPreviewSize = aConfig.mPreviewSize;
aValidatedConfig.mRecorderProfile = profile->GetName();
return NS_OK;
}
nsresult
nsGonkCameraControl::SetConfigurationInternal(const Configuration& aConfig)
{
DOM_CAMERA_LOGT("%s:%d\n", __func__, __LINE__);
nsresult rv;
// Ensure sanity of all provided parameters and determine defaults if
// none are provided when given a new configuration
Configuration config;
nsresult rv = ValidateConfiguration(aConfig, config);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
{
ICameraControlParameterSetAutoEnter set(this);
switch (aConfig.mMode) {
switch (config.mMode) {
case kPictureMode:
rv = SetPictureConfiguration(aConfig);
rv = SetPictureConfiguration(config);
break;
case kVideoMode:
rv = SetVideoConfiguration(aConfig);
rv = SetVideoConfiguration(config);
break;
default:
@ -291,16 +344,15 @@ nsGonkCameraControl::SetConfigurationInternal(const Configuration& aConfig)
return rv;
}
rv = Set(CAMERA_PARAM_RECORDINGHINT, aConfig.mMode == kVideoMode);
rv = Set(CAMERA_PARAM_RECORDINGHINT, config.mMode == kVideoMode);
if (NS_FAILED(rv)) {
DOM_CAMERA_LOGE("Failed to set recording hint (0x%x)\n", rv);
}
}
mCurrentConfiguration.mMode = aConfig.mMode;
mCurrentConfiguration.mRecorderProfile = aConfig.mRecorderProfile;
OnConfigurationChange();
mCurrentConfiguration.mMode = config.mMode;
mCurrentConfiguration.mRecorderProfile = config.mRecorderProfile;
mCurrentConfiguration.mPictureSize = config.mPictureSize;
return NS_OK;
}
@ -335,7 +387,18 @@ nsGonkCameraControl::SetConfigurationImpl(const Configuration& aConfig)
// Restart the preview
DOM_CAMERA_LOGT("%s:%d\n", __func__, __LINE__);
return StartPreviewImpl();
rv = StartPreviewInternal();
if (NS_WARN_IF(NS_FAILED(rv))) {
StopPreviewImpl();
return rv;
}
// OnConfigurationChange() indicates the success case of this operation.
// It must not be fired until all intermediate steps, including starting
// the preview, have completed successfully.
OnConfigurationChange();
OnPreviewStateChange(CameraControlListener::kPreviewStarted);
return NS_OK;
}
nsresult
@ -412,39 +475,29 @@ nsGonkCameraControl::SetPictureConfiguration(const Configuration& aConfig)
DOM_CAMERA_LOGT("%s:%d\n", __func__, __LINE__);
MOZ_ASSERT(NS_GetCurrentThread() == mCameraThread);
nsTArray<Size> sizes;
nsresult rv = Get(CAMERA_PARAM_SUPPORTED_PREVIEWSIZES, sizes);
Size max({0, 0});
nsresult rv = SelectCaptureAndPreviewSize(aConfig.mPreviewSize,
aConfig.mPictureSize, max,
CAMERA_PARAM_PICTURE_SIZE);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
Size preview;
rv = GetSupportedSize(aConfig.mPreviewSize, sizes, preview);
if (NS_FAILED(rv)) {
DOM_CAMERA_LOGE(
"Failed to find a supported preview size, requested size %ux%u (0x%x)",
aConfig.mPreviewSize.width, aConfig.mPreviewSize.height, rv);
return rv;
}
rv = Set(CAMERA_PARAM_PREVIEWSIZE, preview);
if (NS_FAILED(rv)) {
DOM_CAMERA_LOGE("Failed to set supported preview size %ux%u (0x%x)",
preview.width, preview.height, rv);
return rv;
}
mCurrentConfiguration.mPreviewSize = preview;
if (mSeparateVideoAndPreviewSizesSupported) {
MaybeAdjustVideoSize();
}
rv = UpdateThumbnailSize();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
mParams.Get(CAMERA_PARAM_PREVIEWFRAMERATE, mPreviewFps);
DOM_CAMERA_LOGI("picture mode preview: wanted %ux%u, got %ux%u (%u fps)\n",
aConfig.mPreviewSize.width, aConfig.mPreviewSize.height,
preview.width, preview.height,
mCurrentConfiguration.mPreviewSize.width,
mCurrentConfiguration.mPreviewSize.height,
mPreviewFps);
return NS_OK;
@ -683,7 +736,7 @@ nsGonkCameraControl::SetLocation(const Position& aLocation)
}
nsresult
nsGonkCameraControl::StartPreviewImpl()
nsGonkCameraControl::StartPreviewInternal()
{
MOZ_ASSERT(NS_GetCurrentThread() == mCameraThread);
RETURN_IF_NO_CAMERA_HW();
@ -702,10 +755,19 @@ nsGonkCameraControl::StartPreviewImpl()
return NS_ERROR_FAILURE;
}
OnPreviewStateChange(CameraControlListener::kPreviewStarted);
return NS_OK;
}
nsresult
nsGonkCameraControl::StartPreviewImpl()
{
nsresult rv = StartPreviewInternal();
if (NS_SUCCEEDED(rv)) {
OnPreviewStateChange(CameraControlListener::kPreviewStarted);
}
return rv;
}
nsresult
nsGonkCameraControl::StopPreviewImpl()
{
@ -814,8 +876,8 @@ nsGonkCameraControl::SetThumbnailSizeImpl(const Size& aSize)
if (area != 0 &&
delta < smallestDelta &&
supportedSizes[i].width * mLastPictureSize.height ==
mLastPictureSize.width * supportedSizes[i].height) {
supportedSizes[i].width * mCurrentConfiguration.mPictureSize.height ==
mCurrentConfiguration.mPictureSize.width * supportedSizes[i].height) {
smallestDelta = delta;
smallestDeltaIndex = i;
}
@ -898,7 +960,8 @@ nsGonkCameraControl::SetPictureSizeImpl(const Size& aSize)
return NS_ERROR_INVALID_ARG;
}
if (aSize.width == mLastPictureSize.width && aSize.height == mLastPictureSize.height) {
if (aSize.width == mCurrentConfiguration.mPictureSize.width &&
aSize.height == mCurrentConfiguration.mPictureSize.height) {
DOM_CAMERA_LOGI("Requested picture size %ux%u unchanged\n", aSize.width, aSize.height);
return NS_OK;
}
@ -926,7 +989,7 @@ nsGonkCameraControl::SetPictureSizeImpl(const Size& aSize)
return rv;
}
mLastPictureSize = best;
mCurrentConfiguration.mPictureSize = best;
// Finally, update the thumbnail size in case the picture aspect ratio changed.
// Some drivers will fail to take a picture if the thumbnail size is not the
@ -1377,6 +1440,11 @@ nsGonkCameraControl::GetSupportedSize(const Size& aSize,
uint32_t minSizeDelta = UINT32_MAX;
uint32_t delta;
if (aSupportedSizes.IsEmpty()) {
// no valid sizes
return rv;
}
if (!aSize.width && !aSize.height) {
// no size specified, take the first supported size
best = aSupportedSizes[0];
@ -1432,91 +1500,41 @@ nsGonkCameraControl::GetSupportedSize(const Size& aSize,
}
nsresult
nsGonkCameraControl::SetVideoAndPreviewSize(const Size& aPreviewSize, const Size& aVideoSize)
nsGonkCameraControl::SelectCaptureAndPreviewSize(const Size& aPreviewSize,
const Size& aCaptureSize,
const Size& aMaxSize,
uint32_t aCaptureSizeKey)
{
MOZ_ASSERT(NS_GetCurrentThread() == mCameraThread);
MOZ_ASSERT(mSeparateVideoAndPreviewSizesSupported);
DOM_CAMERA_LOGI("Setting video size to %ux%u, preview size to %ux%u\n",
aVideoSize.width, aVideoSize.height,
aPreviewSize.width, aPreviewSize.height);
// At this point, we know the capture size has been validated and replaced
// if necessary with the best matching supported value.
DOM_CAMERA_LOGI("Select capture size %ux%u, preview size %ux%u, maximum size %ux%u\n",
aCaptureSize.width, aCaptureSize.height,
aPreviewSize.width, aPreviewSize.height,
aMaxSize.width, aMaxSize.height);
Size oldSize;
nsresult rv = Get(CAMERA_PARAM_PREVIEWSIZE, oldSize);
nsAutoTArray<Size, 16> sizes;
nsresult rv = Get(CAMERA_PARAM_SUPPORTED_PREVIEWSIZES, sizes);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = Set(CAMERA_PARAM_PREVIEWSIZE, aPreviewSize);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = Set(CAMERA_PARAM_VIDEOSIZE, aVideoSize);
if (NS_WARN_IF(NS_FAILED(rv))) {
Set(CAMERA_PARAM_VIDEOSIZE, oldSize); // error, try to restore the original preview size
return rv;
// May optionally apply a ceiling to the preview size. Any supported preview
// size with an area larger than the maximum will be ignored regardless of
// aspect ratio or delta to requested preview size.
uint32_t maxArea = aMaxSize.width * aMaxSize.height;
if (maxArea == 0) {
maxArea = UINT32_MAX;
}
mCurrentConfiguration.mPreviewSize = aPreviewSize;
mLastRecorderSize = aVideoSize;
const uint32_t previewArea = aPreviewSize.width * aPreviewSize.height;
return NS_OK;
}
nsresult
nsGonkCameraControl::SelectVideoAndPreviewSize(const Configuration& aConfig, const Size& aVideoSize)
{
MOZ_ASSERT(NS_GetCurrentThread() == mCameraThread);
MOZ_ASSERT(mSeparateVideoAndPreviewSizesSupported);
nsTArray<Size> sizes;
nsresult rv = Get(CAMERA_PARAM_SUPPORTED_VIDEOSIZES, sizes);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
Size video;
rv = GetSupportedSize(aVideoSize, sizes, video);
if (NS_FAILED(rv)) {
DOM_CAMERA_LOGE("Failed to find a supported video size, requested size %ux%u",
aVideoSize.width, aVideoSize.height);
return rv;
}
rv = Get(CAMERA_PARAM_SUPPORTED_PREVIEWSIZES, sizes);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
Size preview;
rv = GetSupportedSize(aConfig.mPreviewSize, sizes, preview);
if (NS_FAILED(rv)) {
DOM_CAMERA_LOGE("Failed to find a supported preview size, requested size %ux%u",
aConfig.mPreviewSize.width, aConfig.mPreviewSize.height);
return rv;
}
Size preferred;
rv = Get(CAMERA_PARAM_PREFERRED_PREVIEWSIZE_FOR_VIDEO, preferred);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
// If the requested preview size has the same aspect ratio as the
// requested video size, *and* is the same size or smaller than
// the preferred video size, then we're done.
const uint32_t preferredArea = preferred.width * preferred.height;
if (video.width * aConfig.mPreviewSize.height == aConfig.mPreviewSize.width * video.height &&
preview.width * preview.height <= preferredArea) {
// We're done: set the video and preview sizes and return...
return SetVideoAndPreviewSize(preview, video);
}
// Otherwise, if the requested preview size is larger than the preferred
// size, or there is an aspect ratio mismatch, then we need to set the
// preview size to the closest size smaller than the preferred size,
// preferably with the same aspect ratio as the requested video size.
// We should select a preview size with the same aspect ratio as the capture
// size and minimize the delta with the requested preview size. If we are
// unable to find any supported preview sizes which match the aspect ratio
// of the capture size, we fallback to only minimizing the delta with the
// requested preview size.
SizeIndex bestSizeMatch = 0; // initializers to keep warnings away
SizeIndex bestSizeMatchWithAspectRatio = 0;
@ -1528,13 +1546,19 @@ nsGonkCameraControl::SelectVideoAndPreviewSize(const Configuration& aConfig, con
for (SizeIndex i = 0; i < sizes.Length(); ++i) {
const Size& s = sizes[i];
const uint32_t area = s.width * s.height;
if (area > preferredArea) {
// preview size must be smaller or equal to the capture size
if (aCaptureSize.width < s.width || aCaptureSize.height < s.height) {
continue;
}
const uint32_t delta = preferredArea - area;
if (s.width * video.height == video.width * s.height) {
const uint32_t area = s.width * s.height;
if (area > maxArea) {
continue;
}
const uint32_t delta = abs(static_cast<long int>(previewArea - area));
if (s.width * aCaptureSize.height == aCaptureSize.width * s.height) {
if (delta == 0) {
// exact match, including aspect ratio--we can stop now
bestSizeMatchWithAspectRatio = i;
@ -1553,19 +1577,41 @@ nsGonkCameraControl::SelectVideoAndPreviewSize(const Configuration& aConfig, con
}
}
Size previewSize;
if (foundSizeMatchWithAspectRatio) {
preview = sizes[bestSizeMatchWithAspectRatio];
previewSize = sizes[bestSizeMatchWithAspectRatio];
} else if (foundSizeMatch) {
DOM_CAMERA_LOGW("Unable to match a preview size with aspect ratio of video size %ux%u\n",
video.width, video.height);
preview = sizes[bestSizeMatch];
DOM_CAMERA_LOGW("Unable to match a preview size with aspect ratio of capture size %ux%u\n",
aCaptureSize.width, aCaptureSize.height);
previewSize = sizes[bestSizeMatch];
} else {
DOM_CAMERA_LOGE("Unable to find a preview size for video size %ux%u\n",
video.width, video.height);
DOM_CAMERA_LOGE("Unable to find a preview size for capture size %ux%u\n",
aCaptureSize.width, aCaptureSize.height);
return NS_ERROR_INVALID_ARG;
}
return SetVideoAndPreviewSize(preview, video);
DOM_CAMERA_LOGI("Setting capture size to %ux%u, preview size to %ux%u\n",
aCaptureSize.width, aCaptureSize.height,
previewSize.width, previewSize.height);
Size oldSize;
rv = Get(CAMERA_PARAM_PREVIEWSIZE, oldSize);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = Set(CAMERA_PARAM_PREVIEWSIZE, previewSize);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = Set(aCaptureSizeKey, aCaptureSize);
if (NS_WARN_IF(NS_FAILED(rv))) {
Set(CAMERA_PARAM_PREVIEWSIZE, oldSize); // error, try to restore the original preview size
return rv;
}
mCurrentConfiguration.mPreviewSize = previewSize;
return NS_OK;
}
nsresult
@ -1573,13 +1619,6 @@ nsGonkCameraControl::SetVideoConfiguration(const Configuration& aConfig)
{
DOM_CAMERA_LOGT("%s:%d\n", __func__, __LINE__);
// The application may cache an old configuration and already have
// a desired recorder profile without checking the capabilities first
nsresult rv = LoadRecorderProfiles();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
RecorderProfile* profile;
if (!mRecorderProfiles.Get(aConfig.mRecorderProfile, &profile)) {
DOM_CAMERA_LOGE("Recorder profile '%s' is not supported\n",
@ -1605,7 +1644,14 @@ nsGonkCameraControl::SetVideoConfiguration(const Configuration& aConfig)
if (mSeparateVideoAndPreviewSizesSupported) {
// The camera supports two video streams: a low(er) resolution preview
// stream and and a potentially high(er) resolution stream for encoding.
rv = SelectVideoAndPreviewSize(aConfig, size);
Size preferred;
rv = Get(CAMERA_PARAM_PREFERRED_PREVIEWSIZE_FOR_VIDEO, preferred);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
rv = SelectCaptureAndPreviewSize(aConfig.mPreviewSize, size, preferred,
CAMERA_PARAM_VIDEOSIZE);
if (NS_FAILED(rv)) {
DOM_CAMERA_LOGE("Failed to set video and preview sizes (0x%x)\n", rv);
return rv;
@ -1623,6 +1669,8 @@ nsGonkCameraControl::SetVideoConfiguration(const Configuration& aConfig)
mCurrentConfiguration.mPreviewSize = size;
}
mLastRecorderSize = size;
rv = Set(CAMERA_PARAM_PREVIEWFRAMERATE, static_cast<int>(fps));
if (NS_FAILED(rv)) {
DOM_CAMERA_LOGE("Failed to set video mode frame rate (0x%x)\n", rv);
@ -1903,9 +1951,12 @@ nsGonkCameraControl::LoadRecorderProfiles()
return NS_ERROR_NOT_AVAILABLE;
}
nsTArray<RecorderProfile>::size_type bestIndexMatch = 0;
int bestAreaMatch = 0;
// Limit profiles to those video sizes supported by the camera hardware...
for (nsTArray<RecorderProfile>::size_type i = 0; i < profiles.Length(); ++i) {
int width = profiles[i]->GetVideo().GetSize().width;
int width = profiles[i]->GetVideo().GetSize().width;
int height = profiles[i]->GetVideo().GetSize().height;
if (width < 0 || height < 0) {
DOM_CAMERA_LOGW("Ignoring weird profile '%s' with width and/or height < 0\n",
@ -1916,10 +1967,22 @@ nsGonkCameraControl::LoadRecorderProfiles()
if (static_cast<uint32_t>(width) == sizes[n].width &&
static_cast<uint32_t>(height) == sizes[n].height) {
mRecorderProfiles.Put(profiles[i]->GetName(), profiles[i]);
int area = width * height;
if (area > bestAreaMatch) {
bestIndexMatch = i;
bestAreaMatch = area;
}
break;
}
}
}
// Default profile is the one with the largest area.
if (bestAreaMatch > 0) {
nsAutoString name;
name.AssignASCII("default");
mRecorderProfiles.Put(name, profiles[bestIndexMatch]);
}
}
return NS_OK;

View File

@ -105,10 +105,12 @@ protected:
nsresult Initialize();
nsresult ValidateConfiguration(const Configuration& aConfig, Configuration& aValidatedConfig);
nsresult SetConfigurationInternal(const Configuration& aConfig);
nsresult SetPictureConfiguration(const Configuration& aConfig);
nsresult SetVideoConfiguration(const Configuration& aConfig);
nsresult StartInternal(const Configuration* aInitialConfig);
nsresult StartPreviewInternal();
nsresult StopInternal();
template<class T> nsresult SetAndPush(uint32_t aKey, const T& aValue);
@ -133,8 +135,8 @@ protected:
nsresult SetupRecording(int aFd, int aRotation, uint64_t aMaxFileSizeBytes,
uint64_t aMaxVideoLengthMs);
nsresult SetupRecordingFlash(bool aAutoEnableLowLightTorch);
nsresult SelectVideoAndPreviewSize(const Configuration& aConfig, const Size& aVideoSize);
nsresult SetVideoAndPreviewSize(const Size& aPreviewSize, const Size& aVideoSize);
nsresult SelectCaptureAndPreviewSize(const Size& aPreviewSize, const Size& aCaptureSize,
const Size& aMaxSize, uint32_t aCaptureSizeKey);
nsresult MaybeAdjustVideoSize();
nsresult PausePreview();
nsresult GetSupportedSize(const Size& aSize, const nsTArray<Size>& supportedSizes, Size& best);
@ -158,7 +160,6 @@ protected:
android::sp<android::GonkCameraHardware> mCameraHw;
Size mLastPictureSize;
Size mLastThumbnailSize;
Size mLastRecorderSize;
uint32_t mPreviewFps;

View File

@ -145,6 +145,7 @@ public:
struct Configuration {
Mode mMode;
Size mPreviewSize;
Size mPictureSize;
nsString mRecorderProfile;
};

View File

@ -13,3 +13,5 @@ support-files = camera_common.js
[test_bug1022766.html]
[test_bug1037322.html]
[test_bug1099390.html]
[test_bug1104913.html]
[test_camera_bad_initial_config.html]

View File

@ -15,7 +15,7 @@
var whichCamera = navigator.mozCameras.getListOfCameras()[0];
var config = {
mode: 'picture',
recorderProfile: 'cif',
recorderProfile: 'high',
previewSize: {
width: 352,
height: 288

View File

@ -15,10 +15,10 @@
var whichCamera = navigator.mozCameras.getListOfCameras()[0];
var config = {
mode: 'picture',
recorderProfile: 'cif',
recorderProfile: 'high',
previewSize: {
width: 352,
height: 288
width: 320,
height: 240
}
};
@ -57,14 +57,17 @@ var Camera = {
ok(cfg.mode === "unspecified", "Initial mode = " + cfg.mode);
ok(cfg.previewSize.width === 0 && cfg.previewSize.height === 0,
"Initial preview size = " + cfg.previewSize.width + "x" + cfg.previewSize.height);
ok(cfg.recorderProfile === "",
ok(cfg.recorderProfile === "default",
"Initial recorder profile = '" + cfg.recorderProfile + "'");
// Apply our specific configuration
camera.setConfiguration(config).then(setConfig_onSuccess, onError);
}
navigator.mozCameras.getCamera(whichCamera, {}).then(getCamera_onSuccess, onError);
var cfg = {
mode: 'unspecified',
};
navigator.mozCameras.getCamera(whichCamera, cfg).then(getCamera_onSuccess, onError);
}
}

View File

@ -15,7 +15,7 @@
var whichCamera = navigator.mozCameras.getListOfCameras()[0];
var config = {
mode: 'picture',
recorderProfile: 'cif',
recorderProfile: 'high',
previewSize: {
width: 352,
height: 288

View File

@ -0,0 +1,81 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test for bug 1104913</title>
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="camera_common.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<video id="viewfinder" width="200" height="200" autoplay></video>
<img src="#" alt="This image is going to load" id="testimage"/>
<script class="testbody" type="text/javascript;version=1.7">
var whichCamera = navigator.mozCameras.getListOfCameras()[0];
var config = {
mode: 'picture',
recorderProfile: 'qvga',
pictureSize: {
width: 640,
height: 480
},
previewSize: {
width: 320,
height: 240
}
};
function onError(e) {
ok(false, "Error: " + JSON.stringify(e));
}
var Camera = {
cameraObj: null,
get viewfinder() {
return document.getElementById('viewfinder');
},
start: function test_start() {
function getCamera_onSuccess(d) {
var camera = d.camera;
var cfg = d.configuration;
Camera.cameraObj = camera;
Camera.viewfinder.mozSrcObject = camera;
Camera.viewfinder.play();
// Check the default configuration
ok(cfg.mode === config.mode, "Initial mode = " + cfg.mode);
ok(cfg.previewSize.width === config.previewSize.width &&
cfg.previewSize.height === config.previewSize.height,
"Initial preview size = " + cfg.previewSize.width + "x" + cfg.previewSize.height);
ok(cfg.pictureSize.width === config.pictureSize.width &&
cfg.pictureSize.height === config.pictureSize.height,
"Initial picture size = " + cfg.pictureSize.width + "x" + cfg.pictureSize.height);
ok(cfg.recorderProfile === config.recorderProfile,
"Initial recorder profile = '" + cfg.recorderProfile + "'");
SimpleTest.finish();
}
navigator.mozCameras.getCamera(whichCamera, {}).then(getCamera_onSuccess, onError);
}
}
SimpleTest.waitForExplicitFinish();
window.addEventListener('beforeunload', function() {
Camera.viewfinder.mozSrcObject = null;
if (Camera.cameraObj) {
Camera.cameraObj.release();
Camera.cameraObj = null;
}
});
Camera.start();
</script>
</body>
</html>

View File

@ -17,10 +17,10 @@ const Cr = Components.results;
var whichCamera = navigator.mozCameras.getListOfCameras()[0];
var config = {
mode: 'picture',
recorderProfile: 'cif',
recorderProfile: 'high',
previewSize: {
width: 352,
height: 288
width: 320,
height: 240
}
};
var options = {
@ -137,7 +137,7 @@ var tests = [
next();
}
var recordingOptions = {
profile: 'cif',
profile: 'high',
rotation: 0
};
camera.startRecording(recordingOptions,

View File

@ -14,10 +14,10 @@
var whichCamera = navigator.mozCameras.getListOfCameras()[0];
var options = {
mode: 'picture',
recorderProfile: 'cif',
recorderProfile: 'high',
previewSize: {
width: 352,
height: 288
width: 320,
height: 240
}
};

View File

@ -14,10 +14,10 @@
var whichCamera = navigator.mozCameras.getListOfCameras()[0];
var options = {
mode: 'picture',
recorderProfile: 'cif',
recorderProfile: 'high',
previewSize: {
width: 352,
height: 288
width: 320,
height: 240
}
};

View File

@ -14,7 +14,7 @@
var whichCamera = navigator.mozCameras.getListOfCameras()[0];
var options = {
mode: 'picture',
recorderProfile: 'cif',
recorderProfile: 'high',
previewSize: {
width: 352,
height: 288

View File

@ -0,0 +1,58 @@
<!DOCTYPE HTML>
<html>
<head>
<title>Test for bad initial configuration</title>
<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
<script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="text/javascript" src="camera_common.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
</head>
<body>
<video id="viewfinder" width="200" height="200" autoplay></video>
<img src="#" alt="This image is going to load" id="testimage"/>
<script class="testbody" type="text/javascript;version=1.7">
var whichCamera = navigator.mozCameras.getListOfCameras()[0];
var config = {
mode: 'picture',
recorderProfile: 'foobar',
};
var Camera = {
cameraObj: null,
get viewfinder() {
return document.getElementById('viewfinder');
},
start: function test_start() {
function getCamera_onSuccess(d) {
ok(false, "Get camera should have failed");
SimpleTest.finish();
}
function getCamera_onError(e) {
ok(true, "Get camera failed as expected: " + JSON.stringify(e));
SimpleTest.finish();
}
navigator.mozCameras.getCamera(whichCamera, config).then(getCamera_onSuccess, getCamera_onError);
}
}
SimpleTest.waitForExplicitFinish();
window.addEventListener('beforeunload', function() {
Camera.viewfinder.mozSrcObject = null;
if (Camera.cameraObj) {
Camera.cameraObj.release();
Camera.cameraObj = null;
}
});
Camera.start();
</script>
</body>
</html>

View File

@ -17,7 +17,7 @@
var whichCamera = navigator.mozCameras.getListOfCameras()[0];
var initialConfig = {
mode: 'picture',
recorderProfile: 'cif',
recorderProfile: 'high',
previewSize: {
width: 352,
height: 288

View File

@ -20,7 +20,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=965421
var whichCamera = navigator.mozCameras.getListOfCameras()[0];
var initialConfig = {
mode: 'picture',
recorderProfile: 'cif',
recorderProfile: 'high',
previewSize: {
width: 352,
height: 288

View File

@ -20,7 +20,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=965420
var whichCamera = navigator.mozCameras.getListOfCameras()[0];
var initialConfig = {
mode: 'picture',
recorderProfile: 'cif',
recorderProfile: 'high',
previewSize: {
width: 352,
height: 288

View File

@ -20,7 +20,7 @@ https://bugzilla.mozilla.org/show_bug.cgi?id=940424
var whichCamera = navigator.mozCameras.getListOfCameras()[0];
var initialConfig = {
mode: 'picture',
recorderProfile: 'cif',
recorderProfile: 'high',
previewSize: {
width: 352,
height: 288

View File

@ -22,7 +22,7 @@ SimpleTest.waitForExplicitFinish();
var whichCamera = navigator.mozCameras.getListOfCameras()[0];
var initialConfig = {
mode: 'picture',
recorderProfile: 'cif',
recorderProfile: 'high',
previewSize: {
width: 352,
height: 288

View File

@ -362,21 +362,26 @@ MediaEngineGonkVideoSource::OnHardwareStateChange(HardwareState aState,
nsresult aReason)
{
ReentrantMonitorAutoEnter sync(mCallbackMonitor);
if (aState == CameraControlListener::kHardwareClosed) {
// When the first CameraControl listener is added, it gets pushed
// the current state of the camera--normally 'closed'. We only
// pay attention to that state if we've progressed out of the
// allocated state.
if (mState != kAllocated) {
switch (aState) {
case CameraControlListener::kHardwareClosed:
case CameraControlListener::kHardwareOpenFailed:
mState = kReleased;
mCallbackMonitor.Notify();
}
} else {
// Can't read this except on MainThread (ugh)
NS_DispatchToMainThread(WrapRunnable(nsRefPtr<MediaEngineGonkVideoSource>(this),
&MediaEngineGonkVideoSource::GetRotation));
mState = kStarted;
mCallbackMonitor.Notify();
break;
case CameraControlListener::kHardwareOpen:
// Can't read this except on MainThread (ugh)
NS_DispatchToMainThread(WrapRunnable(nsRefPtr<MediaEngineGonkVideoSource>(this),
&MediaEngineGonkVideoSource::GetRotation));
mState = kStarted;
mCallbackMonitor.Notify();
break;
case CameraControlListener::kHardwareUninitialized:
// When the first CameraControl listener is added, it gets pushed
// the current state of the camera--normally 'uninitialized'.
break;
default:
MOZ_ASSERT_UNREACHABLE("Unanticipated camera hardware state");
break;
}
}

View File

@ -558,7 +558,7 @@ MobileConnectionProvider.prototype = {
deliverListenerEvent: function(aName, aArgs) {
let listeners = this._listeners.slice();
for (let listener of listeners) {
if (listeners.indexOf(listener) === -1) {
if (this._listeners.indexOf(listener) === -1) {
continue;
}
let handler = listener[aName];

View File

@ -461,8 +461,8 @@ WifiGeoPositionProvider.prototype = {
},
sendLocationRequest: function (wifiData) {
let data = {};
if (wifiData) {
let data = { cellTowers: undefined, wifiAccessPoints: undefined };
if (wifiData && wifiData.length >= 2) {
data.wifiAccessPoints = wifiData;
}

View File

@ -15,7 +15,7 @@
if (!this.ctypes) {
// We're likely being loaded as a JSM.
this.EXPORTED_SYMBOLS = [ "libcutils", "libnetutils", "netHelpers" ];
this.EXPORTED_SYMBOLS = [ "libcutils", "netHelpers" ];
Components.utils.import("resource://gre/modules/ctypes.jsm");
}
@ -102,96 +102,6 @@ this.libcutils = (function() {
};
})();
/**
* Network-related functions from libnetutils.
*/
this.libnetutils = (function() {
let library;
try {
library = ctypes.open("libnetutils.so");
} catch(ex) {
if (DEBUG) {
dump("Could not load libnetutils.so!\n");
}
// For now we just fake the ctypes library interfacer to return
// no-op functions when library.declare() is called.
library = {
declare: function() {
return function fake_libnetutils_function() {};
}
};
}
let iface = {
ifc_enable: library.declare("ifc_enable", ctypes.default_abi,
ctypes.int,
ctypes.char.ptr),
ifc_disable: library.declare("ifc_disable", ctypes.default_abi,
ctypes.int,
ctypes.char.ptr),
ifc_add_host_route: library.declare("ifc_add_host_route",
ctypes.default_abi,
ctypes.int,
ctypes.char.ptr,
ctypes.int),
ifc_remove_host_routes: library.declare("ifc_remove_host_routes",
ctypes.default_abi,
ctypes.int,
ctypes.char.ptr),
ifc_set_default_route: library.declare("ifc_set_default_route",
ctypes.default_abi,
ctypes.int,
ctypes.char.ptr,
ctypes.int),
ifc_get_default_route: library.declare("ifc_get_default_route",
ctypes.default_abi,
ctypes.int,
ctypes.char.ptr),
ifc_remove_default_route: library.declare("ifc_remove_default_route",
ctypes.default_abi,
ctypes.int,
ctypes.char.ptr),
ifc_configure: library.declare("ifc_configure", ctypes.default_abi,
ctypes.int,
ctypes.char.ptr,
ctypes.int,
ctypes.int,
ctypes.int,
ctypes.int,
ctypes.int),
ifc_add_route: library.declare("ifc_add_route", ctypes.default_abi,
ctypes.int, // return value
ctypes.char.ptr, // ifname
ctypes.char.ptr, // dst
ctypes.int, // prefix_length
ctypes.char.ptr), // gw
ifc_remove_route: library.declare("ifc_remove_route", ctypes.default_abi,
ctypes.int, // return value
ctypes.char.ptr, // ifname
ctypes.char.ptr, // dst
ctypes.int, // prefix_length
ctypes.char.ptr), // gw
dhcp_stop: library.declare("dhcp_stop", ctypes.default_abi,
ctypes.int,
ctypes.char.ptr),
dhcp_release_lease: library.declare("dhcp_release_lease", ctypes.default_abi,
ctypes.int,
ctypes.char.ptr),
dhcp_get_errmsg: library.declare("dhcp_get_errmsg", ctypes.default_abi,
ctypes.char.ptr),
// Constants for ifc_reset_connections.
// NOTE: Ignored in versions before ICS.
RESET_IPV4_ADDRESSES: 0x01,
RESET_IPV6_ADDRESSES: 0x02,
};
iface.RESET_ALL_ADDRESSES = iface.RESET_IPV4_ADDRESSES |
iface.RESET_IPV6_ADDRESSES;
return iface;
})();
/**
* Helpers for conversions.
*/

View File

@ -10,6 +10,7 @@ interface CameraConfigurationEvent : Event
readonly attribute CameraMode mode;
readonly attribute DOMString recorderProfile;
readonly attribute DOMRectReadOnly? previewSize;
readonly attribute DOMRectReadOnly? pictureSize;
};
dictionary CameraConfigurationEventInit : EventInit
@ -17,4 +18,5 @@ dictionary CameraConfigurationEventInit : EventInit
CameraMode mode = "picture";
DOMString recorderProfile = "cif";
DOMRectReadOnly? previewSize = null;
DOMRectReadOnly? pictureSize = null;
};

View File

@ -255,7 +255,10 @@ interface CameraControl : MediaStream
/* the size of the picture to be returned by a call to takePicture();
an object with 'height' and 'width' properties that corresponds to
one of the options returned by capabilities.pictureSizes. */
one of the options returned by capabilities.pictureSizes.
note that unlike when one uses setConfiguration instead to update the
picture size, this will not recalculate the ideal preview size. */
[Throws]
CameraSize getPictureSize();
[Throws]
@ -287,6 +290,7 @@ interface CameraControl : MediaStream
to the display; e.g. if 'sensorAngle' is 270 degrees (or -90 degrees),
then the preview stream needs to be rotated +90 degrees to have the
same orientation as the real world. */
[Constant, Cached]
readonly attribute long sensorAngle;
/* the mode the camera will use to determine the correct exposure of
@ -360,11 +364,8 @@ interface CameraControl : MediaStream
/* the event dispatched when the camera is successfully configured.
event type is CameraConfigurationEvent where:
'mode' is the selected camera mode
'recorderProfile' is the selected profile
'width' contains the preview width
'height' contains the preview height */
event type is CameraConfigurationEvent which has the same members as
CameraConfiguration. */
attribute EventHandler onconfigurationchange;
/* if focusMode is set to either 'continuous-picture' or 'continuous-video',

View File

@ -15,13 +15,34 @@ dictionary CameraSize
unsigned long height = 0;
};
/* Pre-emptive camera configuration options. */
/* Pre-emptive camera configuration options. If 'mode' is set to "unspecified",
the camera will not be configured immediately. If the 'mode' is set to
"video" or "picture", then the camera automatically configures itself and
will be ready for use upon return.
The remaining parameters are optional and are considered hints by the
camera. The application should use the values returned in the
GetCameraCallback configuration because while the camera makes a best effort
to adhere to the requested values, it may need to change them to ensure
optimal behavior.
If not specified, 'pictureSize' and 'recorderProfile' default to the best or
highest resolutions supported by the camera hardware.
To determine 'previewSize', one should generally provide the size of the
element which will contain the preview rather than guess which supported
preview size is the best. If not specified, 'previewSize' defaults to the
inner window size. */
dictionary CameraConfiguration
{
CameraMode mode = "unspecified";
CameraMode mode = "picture";
CameraSize previewSize = null;
DOMString recorderProfile = ""; // one of the profiles reported by
// CameraControl.capabilities.recorderProfiles
CameraSize pictureSize = null;
/* one of the profiles reported by
CameraControl.capabilities.recorderProfiles
*/
DOMString recorderProfile = "default";
};
[Func="nsDOMCameraManager::HasSupport"]

View File

@ -7,8 +7,8 @@
xmlns:gecko="http://schemas.android.com/apk/res-auto">
<ImageView android:id="@+id/icon"
android:layout_width="48dip"
android:layout_height="48dip"
android:layout_width="@dimen/home_banner_icon_width"
android:layout_height="@dimen/home_banner_icon_height"
android:layout_marginLeft="10dp"
android:scaleType="centerInside"/>
@ -26,7 +26,7 @@
gecko:ellipsizeAtLine="3"/>
<ImageButton android:id="@+id/close"
android:layout_width="34dip"
android:layout_width="@dimen/home_banner_close_width"
android:layout_height="match_parent"
android:background="@drawable/home_banner"
android:scaleType="center"

View File

@ -148,6 +148,9 @@
<!-- Banner -->
<dimen name="home_banner_height">72dp</dimen>
<dimen name="home_banner_close_width">42dp</dimen>
<dimen name="home_banner_icon_height">48dip</dimen>
<dimen name="home_banner_icon_width">48dip</dimen>
<!-- Icon Grid -->
<dimen name="icongrid_columnwidth">128dp</dimen>

View File

@ -765,10 +765,11 @@ class GTestCommands(MachCommandBase):
# current OS.
debugger = mozdebug.get_default_debugger_name(mozdebug.DebuggerSearch.KeepLooking)
debuggerInfo = mozdebug.get_debugger_info(debugger, debugger_args)
if not debuggerInfo:
print("Could not find a suitable debugger in your PATH.")
return 1
if debugger:
debuggerInfo = mozdebug.get_debugger_info(debugger, debugger_args)
if not debuggerInfo:
print("Could not find a suitable debugger in your PATH.")
return 1
# Parameters come from the CLI. We need to convert them before
# their use.
@ -942,10 +943,11 @@ class RunProgram(MachCommandBase):
# current OS.
debugger = mozdebug.get_default_debugger_name(mozdebug.DebuggerSearch.KeepLooking)
self.debuggerInfo = mozdebug.get_debugger_info(debugger, debugparams)
if not self.debuggerInfo:
print("Could not find a suitable debugger in your PATH.")
return 1
if debugger:
self.debuggerInfo = mozdebug.get_debugger_info(debugger, debugparams)
if not self.debuggerInfo:
print("Could not find a suitable debugger in your PATH.")
return 1
# Parameters come from the CLI. We need to convert them before
# their use.

View File

@ -350,12 +350,35 @@ Migrator.prototype = {
// STATE_USER_FXA_VERIFIED state. When the user clicks on the link in
// the mail we should see an ONVERIFIED_NOTIFICATION which will cause us
// to complete the migration.
resendVerificationMail: Task.async(function * () {
resendVerificationMail: Task.async(function * (win) {
// warn if we aren't in the expected state - but go ahead anyway!
if (this._state != this.STATE_USER_FXA_VERIFIED) {
this.log.warn("createFxAccount called in an unexpected state: ${}", this._state);
}
return fxAccounts.resendVerificationEmail();
let ok = true;
try {
yield fxAccounts.resendVerificationEmail();
} catch (ex) {
this.log.error("Failed to resend verification mail: ${}", ex);
ok = false;
}
let fxauser = yield fxAccounts.getSignedInUser();
let sb = Services.strings.createBundle("chrome://browser/locale/accounts.properties");
let heading = ok ?
sb.formatStringFromName("verificationSentHeading", [fxauser.email], 1) :
sb.GetStringFromName("verificationNotSentHeading");
let title = sb.GetStringFromName(ok ? "verificationSentTitle" : "verificationNotSentTitle");
let description = sb.GetStringFromName(ok ? "verificationSentDescription"
: "verificationNotSentDescription");
let factory = Cc["@mozilla.org/prompter;1"]
.getService(Ci.nsIPromptFactory);
let prompt = factory.getPrompt(win, Ci.nsIPrompt);
let bag = prompt.QueryInterface(Ci.nsIWritablePropertyBag2);
bag.setPropertyAsBool("allowTabModal", true);
prompt.alert(title, heading + "\n\n" + description);
}),
// "forget" about the current Firefox account. This should only be called

View File

@ -1801,9 +1801,11 @@ class Mochitest(MochitestUtilsMixin):
# TODO: use mozrunner.local.debugger_arguments:
# https://github.com/mozilla/mozbase/blob/master/mozrunner/mozrunner/local.py#L42
debuggerInfo = mozdebug.get_debugger_info(options.debugger,
options.debuggerArgs,
options.debuggerInteractive)
debuggerInfo = None
if options.debugger:
debuggerInfo = mozdebug.get_debugger_info(options.debugger,
options.debuggerArgs,
options.debuggerInteractive)
if options.useTestMediaDevices:
devices = findTestMediaDevices(self.log)

View File

@ -135,6 +135,7 @@ add_task(function* test_emptytitle_export()
// 9. empty bookmarks db and continue
yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
yield promiseAsyncUpdates();
const NOTITLE_URL = "http://notitle.mozilla.org/";
let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
@ -144,6 +145,7 @@ add_task(function* test_emptytitle_export()
test_bookmarks.unfiled.push({ title: "", url: NOTITLE_URL });
yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
yield promiseAsyncUpdates();
remove_all_bookmarks();
yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
@ -177,6 +179,7 @@ add_task(function* test_import_chromefavicon()
const CHROME_FAVICON_URI_2 = NetUtil.newURI("chrome://global/skin/icons/error-16.png");
yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
yield promiseAsyncUpdates();
let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
PAGE_URI,
PlacesUtils.bookmarks.DEFAULT_INDEX,
@ -201,6 +204,7 @@ add_task(function* test_import_chromefavicon()
{ title: "Test", url: PAGE_URI.spec, icon: base64Icon });
yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
yield promiseAsyncUpdates();
// Change the favicon to check it's really imported again later.
deferred = Promise.defer();
@ -236,7 +240,10 @@ add_task(function* test_import_ontop()
// 4. run the test-suite
yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
yield promiseAsyncUpdates();
yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
yield promiseAsyncUpdates();
yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
yield promiseAsyncUpdates();
yield testImportedBookmarks();

View File

@ -5922,6 +5922,21 @@
"kind": "boolean",
"description": "How many times has the devtool's Responsive View been opened via the toolbox button?"
},
"DEVTOOLS_EYEDROPPER_OPENED_BOOLEAN": {
"expires_in_version": "never",
"kind": "boolean",
"description": "How many times has the devtool's Eyedropper tool been opened?"
},
"DEVTOOLS_MENU_EYEDROPPER_OPENED_BOOLEAN": {
"expires_in_version": "never",
"kind": "boolean",
"description": "How many times has the devtool's eyedropper been opened via the devtools menu?"
},
"DEVTOOLS_PICKER_EYEDROPPER_OPENED_BOOLEAN": {
"expires_in_version": "never",
"kind": "boolean",
"description": "How many times has the devtool's eyedropper been opened via the color picker?"
},
"DEVTOOLS_DEVELOPERTOOLBAR_OPENED_BOOLEAN": {
"expires_in_version": "never",
"kind": "boolean",
@ -6047,6 +6062,21 @@
"kind": "flag",
"description": "How many users have opened the devtool's Responsive View been opened via the toolbox button?"
},
"DEVTOOLS_EYEDROPPER_OPENED_PER_USER_FLAG": {
"expires_in_version": "never",
"kind": "flag",
"description": "How many users have opened the devtool's Eyedropper via the toolbox button?"
},
"DEVTOOLS_MENU_EYEDROPPER_OPENED_PER_USER_FLAG": {
"expires_in_version": "never",
"kind": "flag",
"description": "How many users have opened the devtool's Eyedropper via the devtools menu?"
},
"DEVTOOLS_PICKER_EYEDROPPER_OPENED_PER_USER_FLAG": {
"expires_in_version": "never",
"kind": "flag",
"description": "How many users have opened the devtool's Eyedropper via the color picker?"
},
"DEVTOOLS_DEVELOPERTOOLBAR_OPENED_PER_USER_FLAG": {
"expires_in_version": "never",
"kind": "flag",

View File

@ -6,6 +6,9 @@
// Test that the timeline front's start/stop/isRecording methods work in a
// simple use case, and that markers events are sent when operations occur.
// Note that this test isn't concerned with which markers are actually recorded,
// just that markers are recorded at all.
// Trying to check marker types here may lead to intermittents, see bug 1066474.
const {TimelineFront} = require("devtools/server/actors/timeline");
@ -14,37 +17,40 @@ add_task(function*() {
initDebuggerServer();
let client = new DebuggerClient(DebuggerServer.connectPipe());
let form = yield connectDebuggerClient(client);
let front = TimelineFront(client, form);
ok(front, "The TimelineFront was created");
let isActive = yield front.isRecording();
ok(!isActive, "Not initially recording");
ok(!isActive, "The TimelineFront is not initially recording");
doc.body.innerHeight; // flush any pending reflow
info("Flush any pending reflows");
let forceSyncReflow = doc.body.innerHeight;
info("Start recording");
yield front.start();
isActive = yield front.isRecording();
ok(isActive, "Recording after start()");
ok(isActive, "The TimelineFront is now recording");
info("Change some style on the page to cause style/reflow/paint");
let onMarkers = once(front, "markers");
doc.body.style.padding = "10px";
let markers = yield onMarkers;
let markers = yield once(front, "markers");
ok(true, "The markers event was fired");
ok(markers.length > 0, "Markers were returned");
ok(markers.length > 0, "markers were returned");
ok(markers.some(m => m.name == "Reflow"), "markers includes Reflow");
ok(markers.some(m => m.name == "Paint"), "markers includes Paint");
ok(markers.some(m => m.name == "Styles"), "markers includes Restyle");
info("Flush pending reflows again");
forceSyncReflow = doc.body.innerHeight;
info("Change some style on the page to cause style/paint");
onMarkers = once(front, "markers");
doc.body.style.backgroundColor = "red";
markers = yield once(front, "markers");
markers = yield onMarkers;
ok(markers.length > 0, "markers were returned");
ok(!markers.some(m => m.name == "Reflow"), "markers doesn't include Reflow");
ok(markers.some(m => m.name == "Paint"), "markers includes Paint");
ok(markers.some(m => m.name == "Styles"), "markers includes Restyle");
yield front.stop();

View File

@ -83,6 +83,11 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou
throw new Error('Unsupported version: ' + version);
}
// Some source maps produce relative source paths like "./foo.js" instead of
// "foo.js". Normalize these first so that future comparisons will succeed.
// See bugzil.la/1090768.
sources = sources.map(util.normalize);
// Pass `true` below to allow duplicate names and sources. While source maps
// are intended to be compressed and deduplicated, the TypeScript compiler
// sometimes generates source maps with duplicates in them. See Github issue
@ -307,6 +312,33 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou
return binarySearch.search(aNeedle, aMappings, aComparator);
};
/**
* Compute the last column for each generated mapping. The last column is
* inclusive.
*/
SourceMapConsumer.prototype.computeColumnSpans =
function SourceMapConsumer_computeColumnSpans() {
for (var index = 0; index < this._generatedMappings.length; ++index) {
var mapping = this._generatedMappings[index];
// Mappings do not contain a field for the last generated columnt. We
// can come up with an optimistic estimate, however, by assuming that
// mappings are contiguous (i.e. given two consecutive mappings, the
// first mapping ends where the second one starts).
if (index + 1 < this._generatedMappings.length) {
var nextMapping = this._generatedMappings[index + 1];
if (mapping.generatedLine === nextMapping.generatedLine) {
mapping.lastGeneratedColumn = nextMapping.generatedColumn - 1;
continue;
}
}
// The last mapping for each line spans the entire line.
mapping.lastGeneratedColumn = Infinity;
}
};
/**
* Returns the original source, line, and column information for the generated
* source's line and column positions provided. The only argument is an object
@ -329,23 +361,27 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou
generatedColumn: util.getArg(aArgs, 'column')
};
var mapping = this._findMapping(needle,
this._generatedMappings,
"generatedLine",
"generatedColumn",
util.compareByGeneratedPositions);
var index = this._findMapping(needle,
this._generatedMappings,
"generatedLine",
"generatedColumn",
util.compareByGeneratedPositions);
if (mapping && mapping.generatedLine === needle.generatedLine) {
var source = util.getArg(mapping, 'source', null);
if (source != null && this.sourceRoot != null) {
source = util.join(this.sourceRoot, source);
if (index >= 0) {
var mapping = this._generatedMappings[index];
if (mapping.generatedLine === needle.generatedLine) {
var source = util.getArg(mapping, 'source', null);
if (source != null && this.sourceRoot != null) {
source = util.join(this.sourceRoot, source);
}
return {
source: source,
line: util.getArg(mapping, 'originalLine', null),
column: util.getArg(mapping, 'originalColumn', null),
name: util.getArg(mapping, 'name', null)
};
}
return {
source: source,
line: util.getArg(mapping, 'originalLine', null),
column: util.getArg(mapping, 'originalColumn', null),
name: util.getArg(mapping, 'name', null)
};
}
return {
@ -423,25 +459,82 @@ define('source-map/source-map-consumer', ['require', 'exports', 'module' , 'sou
needle.source = util.relative(this.sourceRoot, needle.source);
}
var mapping = this._findMapping(needle,
this._originalMappings,
"originalLine",
"originalColumn",
util.compareByOriginalPositions);
var index = this._findMapping(needle,
this._originalMappings,
"originalLine",
"originalColumn",
util.compareByOriginalPositions);
if (index >= 0) {
var mapping = this._originalMappings[index];
if (mapping) {
return {
line: util.getArg(mapping, 'generatedLine', null),
column: util.getArg(mapping, 'generatedColumn', null)
column: util.getArg(mapping, 'generatedColumn', null),
lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
};
}
return {
line: null,
column: null
column: null,
lastColumn: null
};
};
/**
* Returns all generated line and column information for the original source
* and line provided. The only argument is an object with the following
* properties:
*
* - source: The filename of the original source.
* - line: The line number in the original source.
*
* and an array of objects is returned, each with the following properties:
*
* - line: The line number in the generated source, or null.
* - column: The column number in the generated source, or null.
*/
SourceMapConsumer.prototype.allGeneratedPositionsFor =
function SourceMapConsumer_allGeneratedPositionsFor(aArgs) {
// When there is no exact match, SourceMapConsumer.prototype._findMapping
// returns the index of the closest mapping less than the needle. By
// setting needle.originalColumn to Infinity, we thus find the last
// mapping for the given line, provided such a mapping exists.
var needle = {
source: util.getArg(aArgs, 'source'),
originalLine: util.getArg(aArgs, 'line'),
originalColumn: Infinity
};
if (this.sourceRoot != null) {
needle.source = util.relative(this.sourceRoot, needle.source);
}
var mappings = [];
var index = this._findMapping(needle,
this._originalMappings,
"originalLine",
"originalColumn",
util.compareByOriginalPositions);
if (index >= 0) {
var mapping = this._originalMappings[index];
while (mapping && mapping.originalLine === needle.originalLine) {
mappings.push({
line: util.getArg(mapping, 'generatedLine', null),
column: util.getArg(mapping, 'generatedColumn', null),
lastColumn: util.getArg(mapping, 'lastGeneratedColumn', null)
});
mapping = this._originalMappings[--index];
}
}
return mappings.reverse();
};
SourceMapConsumer.GENERATED_ORDER = 1;
SourceMapConsumer.ORIGINAL_ORDER = 2;
@ -836,17 +929,17 @@ define('source-map/binary-search', ['require', 'exports', 'module' , ], function
//
// 1. We find the exact element we are looking for.
//
// 2. We did not find the exact element, but we can return the next
// closest element that is less than that element.
// 2. We did not find the exact element, but we can return the index of
// the next closest element that is less than that element.
//
// 3. We did not find the exact element, and there is no next-closest
// element which is less than the one we are searching for, so we
// return null.
// return -1.
var mid = Math.floor((aHigh - aLow) / 2) + aLow;
var cmp = aCompare(aNeedle, aHaystack[mid], true);
if (cmp === 0) {
// Found the element we are looking for.
return aHaystack[mid];
return mid;
}
else if (cmp > 0) {
// aHaystack[mid] is greater than our needle.
@ -856,7 +949,7 @@ define('source-map/binary-search', ['require', 'exports', 'module' , ], function
}
// We did not find an exact match, return the next closest one
// (termination case 2).
return aHaystack[mid];
return mid;
}
else {
// aHaystack[mid] is less than our needle.
@ -866,18 +959,16 @@ define('source-map/binary-search', ['require', 'exports', 'module' , ], function
}
// The exact needle element was not found in this haystack. Determine if
// we are in termination case (2) or (3) and return the appropriate thing.
return aLow < 0
? null
: aHaystack[aLow];
return aLow < 0 ? -1 : aLow;
}
}
/**
* This is an implementation of binary search which will always try and return
* the next lowest value checked if there is no exact hit. This is because
* mappings between original and generated line/col pairs are single points,
* and there is an implicit region between each of them, so a miss just means
* that you aren't on the very start of a region.
* the index of next lowest value checked if there is no exact hit. This is
* because mappings between original and generated line/col pairs are single
* points, and there is an implicit region between each of them, so a miss
* just means that you aren't on the very start of a region.
*
* @param aNeedle The element you are looking for.
* @param aHaystack The array that is being searched.
@ -886,9 +977,10 @@ define('source-map/binary-search', ['require', 'exports', 'module' , ], function
* than, equal to, or greater than the element, respectively.
*/
exports.search = function search(aNeedle, aHaystack, aCompare) {
return aHaystack.length > 0
? recursiveSearch(-1, aHaystack.length, aNeedle, aHaystack, aCompare)
: null;
if (aHaystack.length === 0) {
return -1;
}
return recursiveSearch(-1, aHaystack.length, aNeedle, aHaystack, aCompare)
};
});

File diff suppressed because it is too large Load Diff

View File

@ -142,6 +142,22 @@ define('test/source-map/util', ['require', 'exports', 'module' , 'lib/source-ma
sourceRoot: '/the/root',
mappings: 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA'
};
exports.testMapRelativeSources = {
version: 3,
file: 'min.js',
names: ['bar', 'baz', 'n'],
sources: ['./one.js', './two.js'],
sourcesContent: [
' ONE.foo = function (bar) {\n' +
' return baz(bar);\n' +
' };',
' TWO.inc = function (n) {\n' +
' return n + 1;\n' +
' };'
],
sourceRoot: '/the/root',
mappings: 'CAAC,IAAI,IAAM,SAAUA,GAClB,OAAOC,IAAID;CCDb,IAAI,IAAM,SAAUE,GAClB,OAAOA'
};
exports.emptyMap = {
version: 3,
file: 'min.js',

View File

@ -28,7 +28,7 @@ define("test/source-map/test-binary-search", ["require", "exports", "module"], f
binarySearch.search(needle, haystack, numberCompare);
});
assert.equal(binarySearch.search(needle, haystack, numberCompare), 20);
assert.equal(haystack[binarySearch.search(needle, haystack, numberCompare)], 20);
};
exports['test too low'] = function (assert, util) {
@ -39,21 +39,21 @@ define("test/source-map/test-binary-search", ["require", "exports", "module"], f
binarySearch.search(needle, haystack, numberCompare);
});
assert.equal(binarySearch.search(needle, haystack, numberCompare), null);
assert.equal(binarySearch.search(needle, haystack, numberCompare), -1);
};
exports['test exact search'] = function (assert, util) {
var needle = 4;
var haystack = [2,4,6,8,10,12,14,16,18,20];
assert.equal(binarySearch.search(needle, haystack, numberCompare), 4);
assert.equal(haystack[binarySearch.search(needle, haystack, numberCompare)], 4);
};
exports['test fuzzy search'] = function (assert, util) {
var needle = 19;
var haystack = [2,4,6,8,10,12,14,16,18,20];
assert.equal(binarySearch.search(needle, haystack, numberCompare), 18);
assert.equal(haystack[binarySearch.search(needle, haystack, numberCompare)], 18);
};
});

View File

@ -257,6 +257,25 @@ define("test/source-map/test-source-map-consumer", ["require", "exports", "modul
}, Error);
};
exports['test that we can get the original source content with relative source paths'] = function (assert, util) {
var map = new SourceMapConsumer(util.testMapRelativeSources);
var sources = map.sources;
assert.equal(map.sourceContentFor(sources[0]), ' ONE.foo = function (bar) {\n return baz(bar);\n };');
assert.equal(map.sourceContentFor(sources[1]), ' TWO.inc = function (n) {\n return n + 1;\n };');
assert.equal(map.sourceContentFor("one.js"), ' ONE.foo = function (bar) {\n return baz(bar);\n };');
assert.equal(map.sourceContentFor("two.js"), ' TWO.inc = function (n) {\n return n + 1;\n };');
assert.throws(function () {
map.sourceContentFor("");
}, Error);
assert.throws(function () {
map.sourceContentFor("/the/root/three.js");
}, Error);
assert.throws(function () {
map.sourceContentFor("three.js");
}, Error);
};
exports['test sourceRoot + generatedPositionFor'] = function (assert, util) {
var map = new SourceMapGenerator({
sourceRoot: 'foo/bar',
@ -295,6 +314,158 @@ define("test/source-map/test-source-map-consumer", ["require", "exports", "modul
assert.equal(pos.column, 2);
};
exports['test allGeneratedPositionsFor'] = function (assert, util) {
var map = new SourceMapGenerator({
file: 'generated.js'
});
map.addMapping({
original: { line: 1, column: 1 },
generated: { line: 2, column: 2 },
source: 'foo.coffee'
});
map.addMapping({
original: { line: 1, column: 1 },
generated: { line: 2, column: 2 },
source: 'bar.coffee'
});
map.addMapping({
original: { line: 2, column: 1 },
generated: { line: 3, column: 2 },
source: 'bar.coffee'
});
map.addMapping({
original: { line: 2, column: 2 },
generated: { line: 3, column: 3 },
source: 'bar.coffee'
});
map.addMapping({
original: { line: 3, column: 1 },
generated: { line: 4, column: 2 },
source: 'bar.coffee'
});
map = new SourceMapConsumer(map.toString());
var mappings = map.allGeneratedPositionsFor({
line: 2,
source: 'bar.coffee'
});
assert.equal(mappings.length, 2);
assert.equal(mappings[0].line, 3);
assert.equal(mappings[0].column, 2);
assert.equal(mappings[1].line, 3);
assert.equal(mappings[1].column, 3);
};
exports['test allGeneratedPositionsFor for line with no mappings'] = function (assert, util) {
var map = new SourceMapGenerator({
file: 'generated.js'
});
map.addMapping({
original: { line: 1, column: 1 },
generated: { line: 2, column: 2 },
source: 'foo.coffee'
});
map.addMapping({
original: { line: 1, column: 1 },
generated: { line: 2, column: 2 },
source: 'bar.coffee'
});
map.addMapping({
original: { line: 3, column: 1 },
generated: { line: 4, column: 2 },
source: 'bar.coffee'
});
map = new SourceMapConsumer(map.toString());
var mappings = map.allGeneratedPositionsFor({
line: 2,
source: 'bar.coffee'
});
assert.equal(mappings.length, 0);
};
exports['test allGeneratedPositionsFor source map with no mappings'] = function (assert, util) {
var map = new SourceMapGenerator({
file: 'generated.js'
});
map = new SourceMapConsumer(map.toString());
var mappings = map.allGeneratedPositionsFor({
line: 2,
source: 'bar.coffee'
});
assert.equal(mappings.length, 0);
};
exports['test computeColumnSpans'] = function (assert, util) {
var map = new SourceMapGenerator({
file: 'generated.js'
});
map.addMapping({
original: { line: 1, column: 1 },
generated: { line: 1, column: 1 },
source: 'foo.coffee'
});
map.addMapping({
original: { line: 2, column: 1 },
generated: { line: 2, column: 1 },
source: 'foo.coffee'
});
map.addMapping({
original: { line: 2, column: 2 },
generated: { line: 2, column: 10 },
source: 'foo.coffee'
});
map.addMapping({
original: { line: 2, column: 3 },
generated: { line: 2, column: 20 },
source: 'foo.coffee'
});
map.addMapping({
original: { line: 3, column: 1 },
generated: { line: 3, column: 1 },
source: 'foo.coffee'
});
map.addMapping({
original: { line: 3, column: 2 },
generated: { line: 3, column: 2 },
source: 'foo.coffee'
});
map = new SourceMapConsumer(map.toString());
map.computeColumnSpans();
var mappings = map.allGeneratedPositionsFor({
line: 1,
source: 'foo.coffee'
});
assert.equal(mappings.length, 1);
assert.equal(mappings[0].lastColumn, Infinity);
var mappings = map.allGeneratedPositionsFor({
line: 2,
source: 'foo.coffee'
});
assert.equal(mappings.length, 3);
assert.equal(mappings[0].lastColumn, 9);
assert.equal(mappings[1].lastColumn, 19);
assert.equal(mappings[2].lastColumn, Infinity);
var mappings = map.allGeneratedPositionsFor({
line: 3,
source: 'foo.coffee'
});
assert.equal(mappings.length, 2);
assert.equal(mappings[0].lastColumn, 1);
assert.equal(mappings[1].lastColumn, Infinity);
};
exports['test sourceRoot + originalPositionFor'] = function (assert, util) {
var map = new SourceMapGenerator({
sourceRoot: 'foo/bar',

View File

@ -3,6 +3,7 @@ head = head_sourcemap.js
tail =
support-files = Utils.jsm
[test_util.js]
[test_source_node.js]
[test_source_map_generator.js]
[test_source_map_consumer.js]

View File

@ -584,20 +584,29 @@ xul|filefield + xul|button {
xul|richlistbox,
xul|listbox {
-moz-appearance: none;
background-color: #fff;
border: 1px solid #c1c1c1;
color: #333;
}
xul|treechildren::-moz-tree-row,
xul|listbox xul|listitem {
padding: 5px;
padding: 0.3em;
margin: 0;
border: none;
border-radius: 0;
background-image: none;
}
xul|treechildren::-moz-tree-row(hover),
xul|listbox xul|listitem:hover {
background-color: rgba(0,149,221,0.25);
}
xul|treechildren::-moz-tree-row(selected),
xul|listbox xul|listitem[selected="true"] {
background-color: #f1f1f1;
background-color: #0095dd;
color: #fff;
}
/* Trees */
@ -652,4 +661,18 @@ xul|treecol:not([hideheader="true"]) > xul|*.treecol-sortdirection[sortDirection
xul|treecol:not([hideheader="true"]) > xul|*.treecol-sortdirection[sortDirection] {
list-style-image: url("chrome://global/skin/in-content/sorter@2x.png");
}
}
/* This is the only way to increase the height of a tree row unfortunately */
xul|treechildren::-moz-tree-row {
min-height: 2em;
}
/* Color needs to be set on tree cell in order to be applied */
xul|treechildren::-moz-tree-cell-text {
color: #333;
}
xul|treechildren::-moz-tree-cell-text(selected) {
color: #fff;
}

View File

@ -154,9 +154,7 @@ HwcComposer2D::Init(hwc_display_t dpy, hwc_surface_t sur, gl::GLContext* aGLCont
mRBSwapSupport = false;
}
if (RegisterHwcEventCallback()) {
EnableVsync(true);
}
RegisterHwcEventCallback();
#else
char propValue[PROPERTY_VALUE_MAX];
property_get("ro.display.colorfill", propValue, "0");
@ -181,17 +179,22 @@ HwcComposer2D::GetInstance()
return sInstance;
}
void
bool
HwcComposer2D::EnableVsync(bool aEnable)
{
#if ANDROID_VERSION >= 17
if (NS_IsMainThread()) {
RunVsyncEventControl(aEnable);
} else {
nsRefPtr<nsIRunnable> event =
NS_NewRunnableMethodWithArg<bool>(this, &HwcComposer2D::RunVsyncEventControl, aEnable);
NS_DispatchToMainThread(event);
MOZ_ASSERT(NS_IsMainThread());
if (!mHasHWVsync) {
return false;
}
HwcDevice* device = (HwcDevice*)GetGonkDisplay()->GetHWCDevice();
if (!device) {
return false;
}
device->eventControl(device, HWC_DISPLAY_PRIMARY, HWC_EVENT_VSYNC, aEnable);
return aEnable;
#endif
}
@ -208,27 +211,10 @@ HwcComposer2D::RegisterHwcEventCallback()
// Disable Vsync first, and then register callback functions.
device->eventControl(device, HWC_DISPLAY_PRIMARY, HWC_EVENT_VSYNC, false);
device->registerProcs(device, &sHWCProcs);
mHasHWVsync = true;
if (!gfxPrefs::HardwareVsyncEnabled()) {
device->eventControl(device, HWC_DISPLAY_PRIMARY, HWC_EVENT_VSYNC, false);
mHasHWVsync = false;
}
mHasHWVsync = gfxPrefs::HardwareVsyncEnabled();
return mHasHWVsync;
}
void
HwcComposer2D::RunVsyncEventControl(bool aEnable)
{
if (mHasHWVsync) {
HwcDevice* device = (HwcDevice*)GetGonkDisplay()->GetHWCDevice();
if (device && device->eventControl) {
device->eventControl(device, HWC_DISPLAY_PRIMARY, HWC_EVENT_VSYNC, aEnable);
}
}
}
void
HwcComposer2D::Vsync(int aDisplay, nsecs_t aVsyncTimestamp)
{

View File

@ -91,7 +91,7 @@ public:
bool Render(EGLDisplay dpy, EGLSurface sur);
void EnableVsync(bool aEnable);
bool EnableVsync(bool aEnable);
#if ANDROID_VERSION >= 17
bool RegisterHwcEventCallback();
void Vsync(int aDisplay, int64_t aTimestamp);
@ -111,10 +111,6 @@ private:
void setHwcGeometry(bool aGeometryChanged);
void SendtoLayerScope();
#if ANDROID_VERSION >= 17
void RunVsyncEventControl(bool aEnable);
#endif
HwcDevice* mHwc;
HwcList* mList;
hwc_display_t mDpy;