Merge latest green b2g-inbound changeset and mozilla-central
@ -1176,7 +1176,7 @@ pref("devtools.profiler.ui.show-platform-data", false);
|
||||
pref("devtools.netmonitor.enabled", true);
|
||||
|
||||
// The default Network Monitor UI settings
|
||||
pref("devtools.netmonitor.panes-network-details-width", 450);
|
||||
pref("devtools.netmonitor.panes-network-details-width", 550);
|
||||
pref("devtools.netmonitor.panes-network-details-height", 450);
|
||||
pref("devtools.netmonitor.statistics", true);
|
||||
pref("devtools.netmonitor.filters", "[\"all\"]");
|
||||
|
@ -14,6 +14,10 @@
|
||||
%endif
|
||||
}
|
||||
|
||||
#main-window[customize-entered] {
|
||||
min-width: -moz-fit-content;
|
||||
}
|
||||
|
||||
searchbar {
|
||||
-moz-binding: url("chrome://browser/content/search/search.xml#searchbar");
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ Cu.import("resource://gre/modules/PageThumbs.jsm");
|
||||
Cu.import("resource://gre/modules/BackgroundPageThumbs.jsm");
|
||||
Cu.import("resource://gre/modules/DirectoryLinksProvider.jsm");
|
||||
Cu.import("resource://gre/modules/NewTabUtils.jsm");
|
||||
Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "Rect",
|
||||
"resource://gre/modules/Geometry.jsm");
|
||||
|
@ -181,7 +181,7 @@ let gTransformation = {
|
||||
|
||||
let deferred = Promise.defer();
|
||||
batch.push(deferred.promise);
|
||||
let cb = function () deferred.resolve();
|
||||
let cb = deferred.resolve;
|
||||
|
||||
if (!cells[aIndex])
|
||||
// The site disappeared from the grid, hide it.
|
||||
@ -194,8 +194,9 @@ let gTransformation = {
|
||||
this._moveSite(aSite, aIndex, {unfreeze: unfreeze, callback: cb});
|
||||
}, this);
|
||||
|
||||
let wait = Promise.promised(function () callback && callback());
|
||||
wait.apply(null, batch);
|
||||
if (callback) {
|
||||
Promise.all(batch).then(callback);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -147,8 +147,7 @@ let gUpdater = {
|
||||
});
|
||||
});
|
||||
|
||||
let wait = Promise.promised(aCallback);
|
||||
wait.apply(null, batch);
|
||||
Promise.all(batch).then(aCallback);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -180,7 +179,6 @@ let gUpdater = {
|
||||
gTransformation.showSite(site, function () deferred.resolve());
|
||||
});
|
||||
|
||||
let wait = Promise.promised(aCallback);
|
||||
wait.apply(null, batch);
|
||||
Promise.all(batch).then(aCallback);
|
||||
}
|
||||
};
|
||||
|
@ -1101,6 +1101,22 @@
|
||||
this._tabAttrModified(oldTab);
|
||||
this._tabAttrModified(this.mCurrentTab);
|
||||
|
||||
if (oldBrowser != newBrowser &&
|
||||
oldBrowser.docShell &&
|
||||
oldBrowser.docShell.contentViewer.inPermitUnload) {
|
||||
// Since the user is switching away from a tab that has
|
||||
// a beforeunload prompt active, we remove the prompt.
|
||||
// This prevents confusing user flows like the following:
|
||||
// 1. User attempts to close Firefox
|
||||
// 2. User switches tabs (ingoring a beforeunload prompt)
|
||||
// 3. User returns to tab, presses "Leave page"
|
||||
let promptBox = this.getTabModalPromptBox(oldBrowser);
|
||||
let prompts = promptBox.listPrompts();
|
||||
// NB: This code assumes that the beforeunload prompt
|
||||
// is the top-most prompt on the tab.
|
||||
promptBox.removePrompt(prompts[prompts.length - 1]);
|
||||
}
|
||||
|
||||
// Adjust focus
|
||||
oldBrowser._urlbarFocused = (gURLBar && gURLBar.focused);
|
||||
do {
|
||||
@ -1879,7 +1895,6 @@
|
||||
<body>
|
||||
<![CDATA[
|
||||
if (aTab.closing ||
|
||||
aTab._pendingPermitUnload ||
|
||||
this._windowIsClosing)
|
||||
return false;
|
||||
|
||||
@ -1887,16 +1902,10 @@
|
||||
|
||||
if (!aTabWillBeMoved) {
|
||||
let ds = browser.docShell;
|
||||
if (ds && ds.contentViewer) {
|
||||
// We need to block while calling permitUnload() because it
|
||||
// processes the event queue and may lead to another removeTab()
|
||||
// call before permitUnload() even returned.
|
||||
aTab._pendingPermitUnload = true;
|
||||
let permitUnload = ds.contentViewer.permitUnload();
|
||||
delete aTab._pendingPermitUnload;
|
||||
|
||||
if (!permitUnload)
|
||||
return false;
|
||||
if (ds &&
|
||||
ds.contentViewer &&
|
||||
!ds.contentViewer.permitUnload()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -355,7 +355,7 @@ skip-if = true # disabled until the tree view is added
|
||||
[browser_save_link-perwindowpb.js]
|
||||
skip-if = e10s # Bug ?????? - test directly manipulates content (event.target)
|
||||
[browser_save_private_link_perwindowpb.js]
|
||||
skip-if = os == "linux" || e10s # Linux: bug 857427; e10s: Bug 933103 - mochitest's EventUtils.synthesizeMouse functions not e10s friendly
|
||||
skip-if = e10s # e10s: Bug 933103 - mochitest's EventUtils.synthesizeMouse functions not e10s friendly
|
||||
[browser_save_video.js]
|
||||
skip-if = e10s # Bug ?????? - test directly manipulates content (event.target)
|
||||
[browser_scope.js]
|
||||
|
@ -2400,20 +2400,14 @@ let CustomizableUIInternal = {
|
||||
},
|
||||
|
||||
setToolbarVisibility: function(aToolbarId, aIsVisible) {
|
||||
let area = gAreas.get(aToolbarId);
|
||||
if (area.get("type") != CustomizableUI.TYPE_TOOLBAR) {
|
||||
return;
|
||||
}
|
||||
let areaNodes = gBuildAreas.get(aToolbarId);
|
||||
if (!areaNodes) {
|
||||
return;
|
||||
}
|
||||
// We only persist the attribute the first time.
|
||||
let isFirstChangedToolbar = true;
|
||||
for (let areaNode of areaNodes) {
|
||||
let window = areaNode.ownerDocument.defaultView;
|
||||
window.setToolbarVisibility(areaNode, aIsVisible, isFirstChangedToolbar);
|
||||
isFirstChangedToolbar = false;
|
||||
for (let window of CustomizableUI.windows) {
|
||||
let toolbar = window.document.getElementById(aToolbarId);
|
||||
if (toolbar) {
|
||||
window.setToolbarVisibility(toolbar, aIsVisible, isFirstChangedToolbar);
|
||||
isFirstChangedToolbar = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -1603,6 +1603,7 @@ CustomizeMode.prototype = {
|
||||
}
|
||||
this._updateToolbarCustomizationOutline(this.window);
|
||||
this._showPanelCustomizationPlaceholders();
|
||||
DragPositionManager.stop();
|
||||
},
|
||||
|
||||
_isUnwantedDragDrop: function(aEvent) {
|
||||
|
@ -332,20 +332,36 @@ AreaPositionManager.prototype = {
|
||||
_lazyStoreGet: function(aNode) {
|
||||
let rect = this._nodePositionStore.get(aNode);
|
||||
if (!rect) {
|
||||
rect = aNode.getBoundingClientRect();
|
||||
// getBoundingClientRect() returns a DOMRect that is live, meaning that
|
||||
// as the element moves around, the rects values change. We don't want
|
||||
// that - we want a snapshot of what the rect values are right at this
|
||||
// moment, and nothing else. So we have to clone the values.
|
||||
let clientRect = aNode.getBoundingClientRect();
|
||||
rect = {
|
||||
left: clientRect.left,
|
||||
right: clientRect.right,
|
||||
width: clientRect.width,
|
||||
height: clientRect.height,
|
||||
top: clientRect.top,
|
||||
bottom: clientRect.bottom,
|
||||
};
|
||||
rect.x = rect.left + rect.width / 2;
|
||||
rect.y = rect.top + rect.height / 2;
|
||||
Object.freeze(rect);
|
||||
this._nodePositionStore.set(aNode, rect);
|
||||
}
|
||||
return rect;
|
||||
},
|
||||
|
||||
_firstInRow: function(aNode) {
|
||||
let bound = this._lazyStoreGet(aNode).top;
|
||||
// XXXmconley: I'm not entirely sure why we need to take the floor of these
|
||||
// values - it looks like, periodically, we're getting fractional pixels back
|
||||
//from lazyStoreGet. I've filed bug 994247 to investigate.
|
||||
let bound = Math.floor(this._lazyStoreGet(aNode).top);
|
||||
let rv = aNode;
|
||||
let prev;
|
||||
while (rv && (prev = this._getVisibleSiblingForDirection(rv, "previous"))) {
|
||||
if (this._lazyStoreGet(prev).bottom <= bound) {
|
||||
if (Math.floor(this._lazyStoreGet(prev).bottom) <= bound) {
|
||||
return rv;
|
||||
}
|
||||
rv = prev;
|
||||
|
@ -104,4 +104,5 @@ skip-if = os == "linux"
|
||||
[browser_987177_xul_wrapper_updating.js]
|
||||
[browser_987492_window_api.js]
|
||||
[browser_989289_force_icons_mode_attribute.js]
|
||||
[browser_992747_toggle_noncustomizable_toolbar.js]
|
||||
[browser_panel_toggle.js]
|
||||
|
@ -0,0 +1,26 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const TOOLBARID = "test-noncustomizable-toolbar-for-toggling";
|
||||
function test() {
|
||||
let tb = document.createElementNS(kNSXUL, "toolbar");
|
||||
tb.id = TOOLBARID;
|
||||
gNavToolbox.appendChild(tb);
|
||||
try {
|
||||
CustomizableUI.setToolbarVisibility(TOOLBARID, false);
|
||||
} catch (ex) {
|
||||
ok(false, "Should not throw exceptions trying to set toolbar visibility.");
|
||||
}
|
||||
is(tb.getAttribute("collapsed"), "true", "Toolbar should be collapsed");
|
||||
try {
|
||||
CustomizableUI.setToolbarVisibility(TOOLBARID, true);
|
||||
} catch (ex) {
|
||||
ok(false, "Should not throw exceptions trying to set toolbar visibility.");
|
||||
}
|
||||
is(tb.getAttribute("collapsed"), "false", "Toolbar should be uncollapsed");
|
||||
tb.remove();
|
||||
};
|
||||
|
@ -275,10 +275,7 @@
|
||||
<vbox id="fxa-pweng-help">
|
||||
<spacer flex="1"/>
|
||||
<hbox id="fxa-pweng-help-link">
|
||||
<label value=" ["/>
|
||||
<label class="text-link" value="?"
|
||||
onclick="gSyncUtils.openMPInfoPage(event);"/>
|
||||
<label value="]"/>
|
||||
<image onclick="gSyncUtils.openMPInfoPage(event);" />
|
||||
</hbox>
|
||||
<spacer flex="1"/>
|
||||
</vbox>
|
||||
|
@ -276,10 +276,7 @@
|
||||
<vbox id="fxa-pweng-help">
|
||||
<spacer flex="1"/>
|
||||
<hbox id="fxa-pweng-help-link">
|
||||
<label value=" ["/>
|
||||
<label class="text-link" value="?"
|
||||
onclick="gSyncUtils.openMPInfoPage(event);"/>
|
||||
<label value="]"/>
|
||||
<image onclick="gSyncUtils.openMPInfoPage(event);" />
|
||||
</hbox>
|
||||
<spacer flex="1"/>
|
||||
</vbox>
|
||||
|
@ -1,6 +1,8 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
const TEST_URL = 'data:text/html,<script>window.onbeforeunload=' +
|
||||
'function(e){e.returnValue="?"}</script>';
|
||||
|
||||
@ -20,7 +22,10 @@ function onTabViewShown() {
|
||||
}
|
||||
|
||||
function testStayOnPage(contentWindow, groupItemOne, groupItemTwo) {
|
||||
whenDialogOpened(function (dialog) {
|
||||
// We created a new tab group with a second tab above, so let's
|
||||
// pick that second tab here and wait for its onbeforeunload dialog.
|
||||
let browser = gBrowser.browsers[1];
|
||||
waitForOnBeforeUnloadDialog(browser, function (btnLeave, btnStay) {
|
||||
executeSoon(function () {
|
||||
is(gBrowser.tabs.length, 2,
|
||||
"The total number of tab is 2 when staying on the page");
|
||||
@ -34,14 +39,17 @@ function testStayOnPage(contentWindow, groupItemOne, groupItemTwo) {
|
||||
});
|
||||
|
||||
// stay on page
|
||||
dialog.cancelDialog();
|
||||
btnStay.click();
|
||||
});
|
||||
|
||||
closeGroupItem(groupItemTwo);
|
||||
}
|
||||
|
||||
function testLeavePage(contentWindow, groupItemOne, groupItemTwo) {
|
||||
whenDialogOpened(function (dialog) {
|
||||
// The second tab hasn't been closed yet because we chose to stay. Wait
|
||||
// for the onbeforeunload dialog again and leave the page this time.
|
||||
let browser = gBrowser.browsers[1];
|
||||
waitForOnBeforeUnloadDialog(browser, function (btnLeave, btnStay) {
|
||||
// clean up and finish the test
|
||||
groupItemTwo.addSubscriber("close", function onClose() {
|
||||
groupItemTwo.removeSubscriber("close", onClose);
|
||||
@ -55,31 +63,8 @@ function testLeavePage(contentWindow, groupItemOne, groupItemTwo) {
|
||||
});
|
||||
|
||||
// Leave page
|
||||
dialog.acceptDialog();
|
||||
btnLeave.click();
|
||||
});
|
||||
|
||||
closeGroupItem(groupItemTwo);
|
||||
}
|
||||
|
||||
// ----------
|
||||
function whenDialogOpened(callback) {
|
||||
let listener = {
|
||||
onCloseWindow: function () {},
|
||||
onWindowTitleChange: function () {},
|
||||
|
||||
onOpenWindow: function (xulWin) {
|
||||
let domWin = xulWin.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindow);
|
||||
|
||||
whenWindowLoaded(domWin, function () {
|
||||
let dialog = domWin.document.querySelector("dialog");
|
||||
if (dialog) {
|
||||
Services.wm.removeListener(listener);
|
||||
callback(dialog);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Services.wm.addListener(listener);
|
||||
}
|
||||
|
@ -7,10 +7,10 @@
|
||||
* Raymond Lee <raymond@appcoast.com>
|
||||
*/
|
||||
|
||||
"use strict";
|
||||
|
||||
const TEST_URL = 'data:text/html,<script>window.onbeforeunload=' +
|
||||
'function(e){e.returnValue="?"}</script><body ' +
|
||||
'onunload="alert(\'onunload\')" onpagehide="' +
|
||||
'alert(\'onpagehide\')"></body>';
|
||||
'function(e){e.returnValue="?"}</script>';
|
||||
|
||||
let contentWindow;
|
||||
let activeGroup;
|
||||
@ -23,25 +23,27 @@ function test() {
|
||||
activeGroup = contentWindow.GroupItems.getActiveGroupItem();
|
||||
|
||||
gBrowser.browsers[0].loadURI("data:text/html,<p>test for bug 626455, tab1");
|
||||
gBrowser.addTab(TEST_URL);
|
||||
|
||||
afterAllTabsLoaded(testStayOnPage);
|
||||
let tab = gBrowser.addTab(TEST_URL);
|
||||
afterAllTabsLoaded(() => testStayOnPage(tab));
|
||||
});
|
||||
}
|
||||
|
||||
function testStayOnPage() {
|
||||
whenDialogOpened(function (dialog) {
|
||||
function testStayOnPage(blockingTab) {
|
||||
let browser = blockingTab.linkedBrowser;
|
||||
waitForOnBeforeUnloadDialog(browser, function (btnLeave, btnStay) {
|
||||
// stay on page
|
||||
dialog.cancelDialog();
|
||||
btnStay.click();
|
||||
|
||||
executeSoon(function () {
|
||||
showTabView(function () {
|
||||
is(gBrowser.tabs.length, 1,
|
||||
"The total number of tab is 1 when staying on the page");
|
||||
|
||||
let location = gBrowser.browsers[0].currentURI.spec;
|
||||
isnot(location.indexOf("onbeforeunload"), -1,
|
||||
"The open tab is the expected one");
|
||||
// The other initial tab has been closed when trying to close the tab
|
||||
// group. The only tab left is the one with the onbeforeunload dialog.
|
||||
let url = gBrowser.browsers[0].currentURI.spec;
|
||||
ok(url.contains("onbeforeunload"), "The open tab is the expected one");
|
||||
|
||||
is(contentWindow.GroupItems.getActiveGroupItem(), activeGroup,
|
||||
"Active group is still the same");
|
||||
@ -50,7 +52,7 @@ function testStayOnPage() {
|
||||
"Only one group is open");
|
||||
|
||||
// start the next test
|
||||
testLeavePage();
|
||||
testLeavePage(gBrowser.tabs[0]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -58,12 +60,11 @@ function testStayOnPage() {
|
||||
closeGroupItem(activeGroup);
|
||||
}
|
||||
|
||||
function testLeavePage() {
|
||||
let dialogsAccepted = 0;
|
||||
|
||||
whenDialogOpened(function onDialogOpened(dialog) {
|
||||
function testLeavePage(blockingTab) {
|
||||
let browser = blockingTab.linkedBrowser;
|
||||
waitForOnBeforeUnloadDialog(browser, function (btnLeave, btnStay) {
|
||||
// Leave page
|
||||
dialog.acceptDialog();
|
||||
btnLeave.click();
|
||||
});
|
||||
|
||||
whenGroupClosed(activeGroup, finishTest);
|
||||
@ -85,6 +86,8 @@ function finishTest() {
|
||||
is(contentWindow.GroupItems.groupItems.length, 1,
|
||||
"Only one group is open");
|
||||
|
||||
contentWindow = null;
|
||||
activeGroup = null;
|
||||
finish();
|
||||
}
|
||||
|
||||
@ -95,29 +98,3 @@ function whenGroupClosed(group, callback) {
|
||||
callback();
|
||||
});
|
||||
}
|
||||
|
||||
// ----------
|
||||
function whenDialogOpened(callback) {
|
||||
let wm = Cc["@mozilla.org/appshell/window-mediator;1"]
|
||||
.getService(Ci.nsIWindowMediator);
|
||||
|
||||
let listener = {
|
||||
onCloseWindow: function () {},
|
||||
onWindowTitleChange: function () {},
|
||||
|
||||
onOpenWindow: function (xulWin) {
|
||||
let domWin = xulWin.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindow);
|
||||
|
||||
whenWindowLoaded(domWin, function () {
|
||||
let dialog = domWin.document.querySelector("dialog");
|
||||
if (dialog) {
|
||||
wm.removeListener(listener);
|
||||
callback(dialog);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
wm.addListener(listener);
|
||||
}
|
||||
|
@ -81,6 +81,11 @@ function test4() {
|
||||
|
||||
hideTabView(function() {
|
||||
is(gBrowser.tabs.length, 1, "Total number of tabs is 1 after all tests");
|
||||
|
||||
contentWindow = null;
|
||||
contentElement = null;
|
||||
groupItem = null;
|
||||
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
|
||||
|
||||
// Some tests here assume that all restored tabs are loaded without waiting for
|
||||
// the user to bring them to the foreground. We ensure this by resetting the
|
||||
// related preference (see the "firefox.js" defaults file for details).
|
||||
@ -418,3 +420,17 @@ function promiseWindowClosed(win) {
|
||||
win.close();
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
// ----------
|
||||
function waitForOnBeforeUnloadDialog(browser, callback) {
|
||||
browser.addEventListener("DOMWillOpenModalDialog", function onModalDialog() {
|
||||
browser.removeEventListener("DOMWillOpenModalDialog", onModalDialog, true);
|
||||
|
||||
executeSoon(() => {
|
||||
let stack = browser.parentNode;
|
||||
let dialogs = stack.getElementsByTagNameNS(XUL_NS, "tabmodalprompt");
|
||||
let {button0, button1} = dialogs[0].ui;
|
||||
callback(button0, button1);
|
||||
});
|
||||
}, true);
|
||||
}
|
||||
|
@ -527,11 +527,15 @@ let UI = {
|
||||
if (!this.isTabViewVisible() || this._isChangingVisibility)
|
||||
return;
|
||||
|
||||
this._isChangingVisibility = true;
|
||||
|
||||
// another tab might be select if user decides to stay on a page when
|
||||
// a onclose confirmation prompts.
|
||||
GroupItems.removeHiddenGroups();
|
||||
|
||||
// We need to set this after removing the hidden groups because doing so
|
||||
// might show prompts which will cause us to be called again, and we'd get
|
||||
// stuck if we prevent re-entrancy before doing that.
|
||||
this._isChangingVisibility = true;
|
||||
|
||||
TabItems.pausePainting();
|
||||
|
||||
this._reorderTabsOnHide.forEach(function(groupItem) {
|
||||
|
@ -317,7 +317,7 @@ DevTools.prototype = {
|
||||
closeToolbox: function DT_closeToolbox(target) {
|
||||
let toolbox = this._toolboxes.get(target);
|
||||
if (toolbox == null) {
|
||||
return;
|
||||
return promise.reject(null);
|
||||
}
|
||||
return toolbox.destroy();
|
||||
},
|
||||
|
@ -22,7 +22,8 @@ let test = asyncTest(function*() {
|
||||
inspector.sidebar.select("layoutview");
|
||||
yield inspector.sidebar.once("layoutview-ready");
|
||||
yield runTests();
|
||||
yield gDevTools.closeToolbox(toolbox);
|
||||
// TODO: Closing the toolbox in this test leaks - bug 994314
|
||||
// yield gDevTools.closeToolbox(target);
|
||||
});
|
||||
|
||||
addTest("Test that editing margin dynamically updates the document, pressing escape cancels the changes",
|
||||
|
@ -1,3 +1,4 @@
|
||||
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
@ -22,7 +23,8 @@ let test = asyncTest(function*() {
|
||||
inspector.sidebar.select("layoutview");
|
||||
yield inspector.sidebar.once("layoutview-ready");
|
||||
yield runTests();
|
||||
yield gDevTools.closeToolbox(toolbox);
|
||||
// TODO: Closing the toolbox in this test leaks - bug 994314
|
||||
// yield gDevTools.closeToolbox(target);
|
||||
});
|
||||
|
||||
addTest("When all properties are set on the node editing one should work",
|
||||
|
@ -22,7 +22,8 @@ let test = asyncTest(function*() {
|
||||
inspector.sidebar.select("layoutview");
|
||||
yield inspector.sidebar.once("layoutview-ready");
|
||||
yield runTests();
|
||||
yield gDevTools.closeToolbox(toolbox);
|
||||
// TODO: Closing the toolbox in this test leaks - bug 994314
|
||||
// yield gDevTools.closeToolbox(target);
|
||||
});
|
||||
|
||||
addTest("Test that adding a border applies a border style when necessary",
|
||||
|
@ -22,7 +22,8 @@ let test = asyncTest(function*() {
|
||||
inspector.sidebar.select("layoutview");
|
||||
yield inspector.sidebar.once("layoutview-ready");
|
||||
yield runTests();
|
||||
yield gDevTools.closeToolbox(toolbox);
|
||||
// TODO: Closing the toolbox in this test leaks - bug 994314
|
||||
// yield gDevTools.closeToolbox(target);
|
||||
});
|
||||
|
||||
addTest("Test that entering units works",
|
||||
|
@ -184,6 +184,34 @@
|
||||
</menupopup>
|
||||
</menu>
|
||||
|
||||
<menu id="sp-edit-menu" label="&editMenu.label;"
|
||||
accesskey="&editMenu.accesskey;">
|
||||
<menupopup id="sp-menu_editpopup">
|
||||
<menuitem id="menu_undo"/>
|
||||
<menuitem id="menu_redo"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="menu_cut"/>
|
||||
<menuitem id="menu_copy"/>
|
||||
<menuitem id="menu_paste"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="menu_selectAll"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="menu_find"/>
|
||||
<menuitem id="menu_findAgain"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="se-menu-gotoLine"
|
||||
label="&gotoLineCmd.label;"
|
||||
accesskey="&gotoLineCmd.accesskey;"
|
||||
key="key_gotoLine"
|
||||
command="cmd_gotoLine"/>
|
||||
<menuitem id="sp-menu-pprint"
|
||||
label="&pprint.label;"
|
||||
accesskey="&pprint.accesskey;"
|
||||
key="sp-key-pprint"
|
||||
command="sp-cmd-pprint"/>
|
||||
</menupopup>
|
||||
</menu>
|
||||
|
||||
<menu id="sp-view-menu" label="&viewMenu.label;" accesskey="&viewMenu.accesskey;">
|
||||
<menupopup id="sp-menu-viewpopup">
|
||||
<menuitem id="sp-menu-line-numbers"
|
||||
@ -221,34 +249,6 @@
|
||||
</menupopup>
|
||||
</menu>
|
||||
|
||||
<menu id="sp-edit-menu" label="&editMenu.label;"
|
||||
accesskey="&editMenu.accesskey;">
|
||||
<menupopup id="sp-menu_editpopup">
|
||||
<menuitem id="menu_undo"/>
|
||||
<menuitem id="menu_redo"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="menu_cut"/>
|
||||
<menuitem id="menu_copy"/>
|
||||
<menuitem id="menu_paste"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="menu_selectAll"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="menu_find"/>
|
||||
<menuitem id="menu_findAgain"/>
|
||||
<menuseparator/>
|
||||
<menuitem id="se-menu-gotoLine"
|
||||
label="&gotoLineCmd.label;"
|
||||
accesskey="&gotoLineCmd.accesskey;"
|
||||
key="key_gotoLine"
|
||||
command="cmd_gotoLine"/>
|
||||
<menuitem id="sp-menu-pprint"
|
||||
label="&pprint.label;"
|
||||
accesskey="&pprint.accesskey;"
|
||||
key="sp-key-pprint"
|
||||
command="sp-cmd-pprint"/>
|
||||
</menupopup>
|
||||
</menu>
|
||||
|
||||
<menu id="sp-execute-menu" label="&executeMenu.label;"
|
||||
accesskey="&executeMenu.accesskey;">
|
||||
<menupopup id="sp-menu_executepopup">
|
||||
|
@ -193,10 +193,11 @@ function test() {
|
||||
function testEnd() {
|
||||
document.getElementById("developer-toolbar-closebutton").doCommand();
|
||||
let target1 = TargetFactory.forTab(tab1);
|
||||
gDevTools.closeToolbox(target1);
|
||||
gBrowser.removeTab(tab1);
|
||||
gBrowser.removeTab(tab2);
|
||||
finish();
|
||||
gDevTools.closeToolbox(target1).then(() => {
|
||||
gBrowser.removeTab(tab1);
|
||||
gBrowser.removeTab(tab2);
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
|
@ -356,6 +356,7 @@ Experiments.Experiments.prototype = {
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback, Ci.nsIObserver]),
|
||||
|
||||
init: function () {
|
||||
this._shutdown = false;
|
||||
configureLogging();
|
||||
|
||||
gExperimentsEnabled = gPrefs.get(PREF_ENABLED, false);
|
||||
@ -386,10 +387,19 @@ Experiments.Experiments.prototype = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Uninitialize this instance.
|
||||
*
|
||||
* This function is susceptible to race conditions. If it is called multiple
|
||||
* times before the previous uninit() has completed or if it is called while
|
||||
* an init() operation is being performed, the object may get in bad state
|
||||
* and/or deadlock could occur.
|
||||
*
|
||||
* @return Promise<>
|
||||
* The promise is fulfilled when all pending tasks are finished.
|
||||
*/
|
||||
uninit: function () {
|
||||
uninit: Task.async(function* () {
|
||||
yield this._loadTask;
|
||||
|
||||
if (!this._shutdown) {
|
||||
this._stopWatchingAddons();
|
||||
|
||||
@ -406,10 +416,11 @@ Experiments.Experiments.prototype = {
|
||||
|
||||
this._shutdown = true;
|
||||
if (this._mainTask) {
|
||||
return this._mainTask;
|
||||
yield this._mainTask;
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
|
||||
this._log.info("Completed uninitialization.");
|
||||
}),
|
||||
|
||||
_startWatchingAddons: function () {
|
||||
AddonManager.addAddonListener(this);
|
||||
|
@ -135,12 +135,18 @@ browser.jar:
|
||||
#endif
|
||||
* skin/classic/browser/preferences/preferences.css (preferences/preferences.css)
|
||||
* skin/classic/browser/preferences/in-content/preferences.css (preferences/in-content/preferences.css)
|
||||
skin/classic/browser/preferences/in-content/check.png (preferences/in-content/check.png)
|
||||
skin/classic/browser/preferences/in-content/icons.png (preferences/in-content/icons.png)
|
||||
skin/classic/browser/preferences/in-content/header.png (preferences/in-content/header.png)
|
||||
skin/classic/browser/preferences/in-content/dropdown.png (preferences/in-content/dropdown.png)
|
||||
skin/classic/browser/preferences/in-content/sorter.png (preferences/in-content/sorter.png)
|
||||
skin/classic/browser/preferences/in-content/dropdown-disabled.png (preferences/in-content/dropdown-disabled.png)
|
||||
skin/classic/browser/preferences/in-content/check.png (../shared/incontentprefs/check.png)
|
||||
skin/classic/browser/preferences/in-content/check@2x.png (../shared/incontentprefs/check@2x.png)
|
||||
skin/classic/browser/preferences/in-content/icons.png (../shared/incontentprefs/icons.png)
|
||||
skin/classic/browser/preferences/in-content/icons@2x.png (../shared/incontentprefs/icons@2x.png)
|
||||
skin/classic/browser/preferences/in-content/header.png (../shared/incontentprefs/header.png)
|
||||
skin/classic/browser/preferences/in-content/header@2x.png (../shared/incontentprefs/header@2x.png)
|
||||
skin/classic/browser/preferences/in-content/dropdown.png (../shared/incontentprefs/dropdown.png)
|
||||
skin/classic/browser/preferences/in-content/dropdown@2x.png (../shared/incontentprefs/dropdown@2x.png)
|
||||
skin/classic/browser/preferences/in-content/sorter.png (../shared/incontentprefs/sorter.png)
|
||||
skin/classic/browser/preferences/in-content/sorter@2x.png (../shared/incontentprefs/sorter@2x.png)
|
||||
skin/classic/browser/preferences/in-content/dropdown-disabled.png (../shared/incontentprefs/dropdown-disabled.png)
|
||||
skin/classic/browser/preferences/in-content/dropdown-disabled@2x.png (../shared/incontentprefs/dropdown-disabled@2x.png)
|
||||
skin/classic/browser/preferences/applications.css (preferences/applications.css)
|
||||
skin/classic/browser/preferences/aboutPermissions.css (preferences/aboutPermissions.css)
|
||||
skin/classic/browser/social/services-16.png (social/services-16.png)
|
||||
|
Before Width: | Height: | Size: 250 B |
Before Width: | Height: | Size: 250 B |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 12 KiB |
@ -2,7 +2,7 @@
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
- You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
%include ../../../shared/in-content/preferences.css
|
||||
%include ../../../shared/incontentprefs/preferences.css
|
||||
|
||||
button > .button-box,
|
||||
menulist > .menulist-label-box {
|
||||
|
Before Width: | Height: | Size: 154 B |
@ -169,4 +169,8 @@ label.small {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#fxa-pweng-help-link > image {
|
||||
list-style-image: url("chrome://global/skin/icons/question-16.png");
|
||||
}
|
||||
|
||||
%endif
|
||||
|
@ -223,14 +223,18 @@ browser.jar:
|
||||
skin/classic/browser/preferences/saveFile.png (preferences/saveFile.png)
|
||||
* skin/classic/browser/preferences/preferences.css (preferences/preferences.css)
|
||||
* skin/classic/browser/preferences/in-content/preferences.css (preferences/in-content/preferences.css)
|
||||
skin/classic/browser/preferences/in-content/check.png (preferences/in-content/check.png)
|
||||
skin/classic/browser/preferences/in-content/check@2x.png (preferences/in-content/check@2x.png)
|
||||
skin/classic/browser/preferences/in-content/icons.png (preferences/in-content/icons.png)
|
||||
skin/classic/browser/preferences/in-content/icons@2x.png (preferences/in-content/icons@2x.png)
|
||||
skin/classic/browser/preferences/in-content/header.png (preferences/in-content/icons@2x.png)
|
||||
skin/classic/browser/preferences/in-content/sorter.png (preferences/in-content/sorter.png)
|
||||
skin/classic/browser/preferences/in-content/dropdown.png (preferences/in-content/dropdown.png)
|
||||
skin/classic/browser/preferences/in-content/dropdown-disabled.png (preferences/in-content/dropdown-disabled.png)
|
||||
skin/classic/browser/preferences/in-content/check.png (../shared/incontentprefs/check.png)
|
||||
skin/classic/browser/preferences/in-content/check@2x.png (../shared/incontentprefs/check@2x.png)
|
||||
skin/classic/browser/preferences/in-content/icons.png (../shared/incontentprefs/icons.png)
|
||||
skin/classic/browser/preferences/in-content/icons@2x.png (../shared/incontentprefs/icons@2x.png)
|
||||
skin/classic/browser/preferences/in-content/header.png (../shared/incontentprefs/header.png)
|
||||
skin/classic/browser/preferences/in-content/header@2x.png (../shared/incontentprefs/header@2x.png)
|
||||
skin/classic/browser/preferences/in-content/sorter.png (../shared/incontentprefs/sorter.png)
|
||||
skin/classic/browser/preferences/in-content/sorter@2x.png (../shared/incontentprefs/sorter@2x.png)
|
||||
skin/classic/browser/preferences/in-content/dropdown.png (../shared/incontentprefs/dropdown.png)
|
||||
skin/classic/browser/preferences/in-content/dropdown@2x.png (../shared/incontentprefs/dropdown@2x.png)
|
||||
skin/classic/browser/preferences/in-content/dropdown-disabled.png (../shared/incontentprefs/dropdown-disabled.png)
|
||||
skin/classic/browser/preferences/in-content/dropdown-disabled@2x.png (../shared/incontentprefs/dropdown-disabled@2x.png)
|
||||
skin/classic/browser/preferences/applications.css (preferences/applications.css)
|
||||
skin/classic/browser/preferences/aboutPermissions.css (preferences/aboutPermissions.css)
|
||||
skin/classic/browser/social/services-16.png (social/services-16.png)
|
||||
|
Before Width: | Height: | Size: 593 B |
Before Width: | Height: | Size: 250 B |
Before Width: | Height: | Size: 250 B |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 30 KiB |
@ -2,7 +2,7 @@
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
- You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
%include ../../../shared/in-content/preferences.css
|
||||
%include ../../../shared/incontentprefs/preferences.css
|
||||
|
||||
menulist:not([editable="true"]) > .menulist-dropmarker {
|
||||
display: -moz-box;
|
||||
@ -61,75 +61,3 @@ description {
|
||||
font-size: 1.25rem;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
@media (min-resolution: 2dppx) {
|
||||
checkbox:hover::before,
|
||||
checkbox[checked]::before {
|
||||
background-size: cover;
|
||||
background-image: -moz-image-rect(url("chrome://browser/skin/preferences/in-content/check@2x.png"), 0, 30, 30, 0);
|
||||
}
|
||||
|
||||
checkbox[checked]::before {
|
||||
background-image: -moz-image-rect(url("chrome://browser/skin/preferences/in-content/check@2x.png"), 0, 60, 30, 30);
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
list-style-image: url("chrome://browser/skin/preferences/in-content/icons@2x.png");
|
||||
}
|
||||
|
||||
#category-general > .category-icon {
|
||||
-moz-image-region: rect(0, 48px, 48px, 0);
|
||||
}
|
||||
|
||||
#category-general[selected] > .category-icon {
|
||||
-moz-image-region: rect(48px, 48px, 96px, 0);
|
||||
}
|
||||
|
||||
#category-content > .category-icon {
|
||||
-moz-image-region: rect(0, 96px, 48px, 48px);
|
||||
}
|
||||
|
||||
#category-content[selected] > .category-icon {
|
||||
-moz-image-region: rect(48px, 96px, 96px, 48px);
|
||||
}
|
||||
|
||||
#category-application > .category-icon {
|
||||
-moz-image-region: rect(0, 144px, 48px, 96px);
|
||||
}
|
||||
|
||||
#category-application[selected] > .category-icon {
|
||||
-moz-image-region: rect(48px, 144px, 96px, 96px);
|
||||
}
|
||||
|
||||
#category-privacy > .category-icon {
|
||||
-moz-image-region: rect(0, 192px, 48px, 144px);
|
||||
}
|
||||
|
||||
#category-privacy[selected] > .category-icon {
|
||||
-moz-image-region: rect(48px, 192px, 96px, 144px);
|
||||
}
|
||||
|
||||
#category-security > .category-icon {
|
||||
-moz-image-region: rect(0, 240px, 48px, 192px);
|
||||
}
|
||||
|
||||
#category-security[selected] > .category-icon {
|
||||
-moz-image-region: rect(48px, 240px, 96px, 192px);
|
||||
}
|
||||
|
||||
#category-sync > .category-icon {
|
||||
-moz-image-region: rect(0, 288px, 48px, 240px);
|
||||
}
|
||||
|
||||
#category-sync[selected] > .category-icon {
|
||||
-moz-image-region: rect(48px, 288px, 96px, 240px);
|
||||
}
|
||||
|
||||
#category-advanced > .category-icon {
|
||||
-moz-image-region: rect(0, 336px, 48px, 288px);
|
||||
}
|
||||
|
||||
#category-advanced[selected] > .category-icon {
|
||||
-moz-image-region: rect(48px, 336px, 96px, 288px);
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 154 B |
@ -234,4 +234,16 @@ html|a.inline-link:-moz-focusring {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#fxa-pweng-help-link > image {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
list-style-image: url("chrome://global/skin/icons/question-16.png");
|
||||
}
|
||||
|
||||
@media (min-resolution: 2dppx) {
|
||||
#fxa-pweng-help-link > image {
|
||||
list-style-image: url("chrome://global/skin/icons/question-32.png");
|
||||
}
|
||||
}
|
||||
|
||||
%endif
|
||||
|
@ -244,7 +244,6 @@ panelview:not([mainview]) .toolbarbutton-text,
|
||||
|
||||
toolbaritem[cui-areatype="menu-panel"][sdkstylewidget="true"]:not(.panel-wide-item),
|
||||
.panelUI-grid .toolbarbutton-1,
|
||||
toolbarpaletteitem[place="panel"]:not([haswideitem=true]),
|
||||
.panel-customization-placeholder-child {
|
||||
-moz-appearance: none;
|
||||
-moz-box-orient: vertical;
|
||||
@ -772,17 +771,8 @@ menuitem.panel-subview-footer@menuStateActive@,
|
||||
}
|
||||
|
||||
#BMB_bookmarksPopup menupopup > .bookmarks-actions-menuseparator {
|
||||
/* Hide bottom separator as the styled footer includes a top border serving the same purpose.
|
||||
* We can't just use display: none here, otherwise scrollbox.xml will flip out and sometimes
|
||||
* refuse to scroll for us (see bug 984156). Instead, we set it to visibility hidden, force
|
||||
* a minimum height, and then negative-margin that single pixel into oblivion. That seems
|
||||
* to be enough to make scrollbox happy.
|
||||
*/
|
||||
-moz-appearance: none;
|
||||
visibility: hidden;
|
||||
min-height: 1px;
|
||||
margin: -1px 0 0;
|
||||
border: none;
|
||||
/* Hide bottom separator as the styled footer includes a top border serving the same purpose */
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Popups with only one item don't have a footer */
|
||||
@ -918,10 +908,14 @@ toolbarpaletteitem[place="palette"] > #search-container {
|
||||
|
||||
/* Make direct siblings overlap borders: */
|
||||
.toolbaritem-combined-buttons + .toolbaritem-combined-buttons@inAnyPanel@ {
|
||||
margin-top: -1px;
|
||||
border-top-color: transparent !important;
|
||||
}
|
||||
|
||||
.toolbaritem-combined-buttons + .toolbaritem-combined-buttons@inAnyPanel@,
|
||||
toolbarpaletteitem[haswideitem][place="panel"] + toolbarpaletteitem[haswideitem][place="panel"] {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
.toolbaritem-combined-buttons@inAnyPanel@ > toolbarbutton {
|
||||
border: 0;
|
||||
padding: .5em;
|
||||
|
Before Width: | Height: | Size: 593 B After Width: | Height: | Size: 593 B |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
browser/themes/shared/incontentprefs/dropdown-disabled.png
Normal file
After Width: | Height: | Size: 412 B |
BIN
browser/themes/shared/incontentprefs/dropdown-disabled@2x.png
Normal file
After Width: | Height: | Size: 865 B |
BIN
browser/themes/shared/incontentprefs/dropdown.png
Normal file
After Width: | Height: | Size: 421 B |
BIN
browser/themes/shared/incontentprefs/dropdown@2x.png
Normal file
After Width: | Height: | Size: 885 B |
BIN
browser/themes/shared/incontentprefs/header.png
Normal file
After Width: | Height: | Size: 7.5 KiB |
BIN
browser/themes/shared/incontentprefs/header@2x.png
Normal file
After Width: | Height: | Size: 15 KiB |
BIN
browser/themes/shared/incontentprefs/icons.png
Normal file
After Width: | Height: | Size: 8.4 KiB |
BIN
browser/themes/shared/incontentprefs/icons@2x.png
Normal file
After Width: | Height: | Size: 21 KiB |
@ -178,10 +178,10 @@ button[type="menu"] > .button-box > .button-menu-dropmarker {
|
||||
-moz-margin-start: 10px;
|
||||
padding: 0;
|
||||
width: 10px;
|
||||
height: 15px;
|
||||
height: 16px;
|
||||
border: none;
|
||||
background-color: transparent;
|
||||
list-style-image: url("chrome://browser/skin/preferences/in-content/dropdown.png")
|
||||
list-style-image: url("chrome://browser/skin/preferences/in-content/dropdown.png");
|
||||
}
|
||||
|
||||
.spinbuttons-button {
|
||||
@ -232,6 +232,23 @@ menulist[disabled="true"]:not([editable="true"]) > .menulist-dropmarker {
|
||||
list-style-image: url("chrome://browser/skin/preferences/in-content/dropdown-disabled.png")
|
||||
}
|
||||
|
||||
@media (min-resolution: 2dppx) {
|
||||
menulist:not([editable="true"]) > .menulist-dropmarker,
|
||||
button[type="menu"] > .button-box > .button-menu-dropmarker {
|
||||
list-style-image: url("chrome://browser/skin/preferences/in-content/dropdown@2x.png");
|
||||
}
|
||||
|
||||
menulist[disabled="true"]:not([editable="true"]) > .menulist-dropmarker {
|
||||
list-style-image: url("chrome://browser/skin/preferences/in-content/dropdown-disabled@2x.png")
|
||||
}
|
||||
|
||||
menulist:not([editable="true"]) > .menulist-dropmarker > .dropmarker-icon,
|
||||
button[type="menu"] > .button-box > .button-menu-dropmarker > .dropmarker-icon {
|
||||
width: 10px;
|
||||
height: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
menulist > menupopup,
|
||||
button[type="menu"] > menupopup {
|
||||
-moz-appearance: none;
|
||||
@ -365,6 +382,18 @@ checkbox[checked]::before {
|
||||
background-position: -15px 0;
|
||||
}
|
||||
|
||||
@media (min-resolution: 2dppx) {
|
||||
checkbox:hover::before {
|
||||
background-size: cover;
|
||||
background-image: -moz-image-rect(url("chrome://browser/skin/preferences/in-content/check@2x.png"), 0, 30, 30, 0);
|
||||
}
|
||||
|
||||
checkbox[checked]::before {
|
||||
background-size: cover;
|
||||
background-image: -moz-image-rect(url("chrome://browser/skin/preferences/in-content/check@2x.png"), 0, 60, 30, 30);
|
||||
}
|
||||
}
|
||||
|
||||
.radio-check {
|
||||
-moz-appearance: none;
|
||||
width: 23px;
|
||||
@ -509,6 +538,68 @@ radio[selected]::before {
|
||||
-moz-image-region: rect(24px, 168px, 48px, 144px);
|
||||
}
|
||||
|
||||
@media (min-resolution: 2dppx) {
|
||||
.category-icon {
|
||||
list-style-image: url("chrome://browser/skin/preferences/in-content/icons@2x.png");
|
||||
}
|
||||
|
||||
#category-general > .category-icon {
|
||||
-moz-image-region: rect(0, 48px, 48px, 0);
|
||||
}
|
||||
|
||||
#category-general[selected] > .category-icon {
|
||||
-moz-image-region: rect(48px, 48px, 96px, 0);
|
||||
}
|
||||
|
||||
#category-content > .category-icon {
|
||||
-moz-image-region: rect(0, 96px, 48px, 48px);
|
||||
}
|
||||
|
||||
#category-content[selected] > .category-icon {
|
||||
-moz-image-region: rect(48px, 96px, 96px, 48px);
|
||||
}
|
||||
|
||||
#category-application > .category-icon {
|
||||
-moz-image-region: rect(0, 144px, 48px, 96px);
|
||||
}
|
||||
|
||||
#category-application[selected] > .category-icon {
|
||||
-moz-image-region: rect(48px, 144px, 96px, 96px);
|
||||
}
|
||||
|
||||
#category-privacy > .category-icon {
|
||||
-moz-image-region: rect(0, 192px, 48px, 144px);
|
||||
}
|
||||
|
||||
#category-privacy[selected] > .category-icon {
|
||||
-moz-image-region: rect(48px, 192px, 96px, 144px);
|
||||
}
|
||||
|
||||
#category-security > .category-icon {
|
||||
-moz-image-region: rect(0, 240px, 48px, 192px);
|
||||
}
|
||||
|
||||
#category-security[selected] > .category-icon {
|
||||
-moz-image-region: rect(48px, 240px, 96px, 192px);
|
||||
}
|
||||
|
||||
#category-sync > .category-icon {
|
||||
-moz-image-region: rect(0, 288px, 48px, 240px);
|
||||
}
|
||||
|
||||
#category-sync[selected] > .category-icon {
|
||||
-moz-image-region: rect(48px, 288px, 96px, 240px);
|
||||
}
|
||||
|
||||
#category-advanced > .category-icon {
|
||||
-moz-image-region: rect(0, 336px, 48px, 288px);
|
||||
}
|
||||
|
||||
#category-advanced[selected] > .category-icon {
|
||||
-moz-image-region: rect(48px, 336px, 96px, 288px);
|
||||
}
|
||||
}
|
||||
|
||||
/* header */
|
||||
|
||||
.header {
|
||||
@ -530,31 +621,65 @@ radio[selected]::before {
|
||||
}
|
||||
|
||||
#header-general > .header-icon {
|
||||
-moz-image-region: rect(3px, 45px, 45px, 3px);
|
||||
-moz-image-region: rect(0, 40px, 40px, 0);
|
||||
}
|
||||
|
||||
#header-content > .header-icon {
|
||||
-moz-image-region: rect(3px, 93px, 45px, 51px);
|
||||
-moz-image-region: rect(0, 80px, 40px, 40px);
|
||||
}
|
||||
|
||||
#header-application > .header-icon {
|
||||
-moz-image-region: rect(3px, 141px, 45px, 99px);
|
||||
-moz-image-region: rect(0, 120px, 40px, 80px);
|
||||
}
|
||||
|
||||
#header-privacy > .header-icon {
|
||||
-moz-image-region: rect(3px, 189px, 45px, 147px);
|
||||
-moz-image-region: rect(0, 160px, 40px, 120px);
|
||||
}
|
||||
|
||||
#header-security > .header-icon {
|
||||
-moz-image-region: rect(3px, 237px, 45px, 195px);
|
||||
-moz-image-region: rect(0, 200px, 40px, 160px);
|
||||
}
|
||||
|
||||
#header-sync > .header-icon {
|
||||
-moz-image-region: rect(3px, 285px, 45px, 243px);
|
||||
-moz-image-region: rect(0, 240px, 40px, 200px);
|
||||
}
|
||||
|
||||
#header-advanced > .header-icon {
|
||||
-moz-image-region: rect(3px, 333px, 45px, 291px);
|
||||
-moz-image-region: rect(0, 280px, 40px, 240px);
|
||||
}
|
||||
|
||||
@media (min-resolution: 2dppx) {
|
||||
.header-icon {
|
||||
list-style-image: url("chrome://browser/skin/preferences/in-content/header@2x.png");
|
||||
}
|
||||
|
||||
#header-general > .header-icon {
|
||||
-moz-image-region: rect(0, 80px, 80px, 0);
|
||||
}
|
||||
|
||||
#header-content > .header-icon {
|
||||
-moz-image-region: rect(0, 160px, 80px, 80px);
|
||||
}
|
||||
|
||||
#header-application > .header-icon {
|
||||
-moz-image-region: rect(0, 240px, 80px, 160px);
|
||||
}
|
||||
|
||||
#header-privacy > .header-icon {
|
||||
-moz-image-region: rect(0, 320px, 80px, 240px);
|
||||
}
|
||||
|
||||
#header-security > .header-icon {
|
||||
-moz-image-region: rect(0, 400px, 80px, 320px);
|
||||
}
|
||||
|
||||
#header-sync > .header-icon {
|
||||
-moz-image-region: rect(0, 480px, 80px, 400px);
|
||||
}
|
||||
|
||||
#header-advanced > .header-icon {
|
||||
-moz-image-region: rect(0, 560px, 80px, 480px);
|
||||
}
|
||||
}
|
||||
|
||||
.indent {
|
||||
@ -645,7 +770,7 @@ filefield {
|
||||
#typeColumn > .treecol-sortdirection[sortDirection=ascending],
|
||||
#actionColumn > .treecol-sortdirection[sortDirection=ascending],
|
||||
#typeColumn > .treecol-sortdirection[sortDirection=descending],
|
||||
#actionColumn > .treecol-sortdirection[sortDirection=descending] {
|
||||
#actionColumn > .treecol-sortdirection[sortDirection=descending] {
|
||||
-moz-appearance: none;
|
||||
list-style-image: url("chrome://browser/skin/preferences/in-content/sorter.png");
|
||||
}
|
||||
@ -655,6 +780,17 @@ filefield {
|
||||
transform: scaleY(-1);
|
||||
}
|
||||
|
||||
@media (min-resolution: 2dppx) {
|
||||
#typeColumn > .treecol-sortdirection[sortDirection=ascending],
|
||||
#actionColumn > .treecol-sortdirection[sortDirection=ascending],
|
||||
#typeColumn > .treecol-sortdirection[sortDirection=descending],
|
||||
#actionColumn > .treecol-sortdirection[sortDirection=descending] {
|
||||
width: 12px;
|
||||
height: 8px;
|
||||
list-style-image: url("chrome://browser/skin/preferences/in-content/sorter@2x.png");
|
||||
}
|
||||
}
|
||||
|
||||
#handlersView > richlistitem {
|
||||
min-height: 40px !important;
|
||||
}
|
BIN
browser/themes/shared/incontentprefs/sorter.png
Normal file
After Width: | Height: | Size: 264 B |
BIN
browser/themes/shared/incontentprefs/sorter@2x.png
Normal file
After Width: | Height: | Size: 523 B |
@ -58,8 +58,6 @@
|
||||
border-color: hsla(210,54%,20%,.25) hsla(210,54%,20%,.27) hsla(210,54%,20%,.3);
|
||||
box-shadow: 0 1px 0 hsla(0,0%,0%,.01) inset,
|
||||
0 1px 0 hsla(0,0%,100%,.1);
|
||||
transition-property: border-color;
|
||||
transition-duration: 200ms;
|
||||
}
|
||||
|
||||
#urlbar:not(:-moz-lwtheme)[focused],
|
||||
|
@ -160,14 +160,18 @@ browser.jar:
|
||||
skin/classic/browser/preferences/saveFile.png (preferences/saveFile.png)
|
||||
* skin/classic/browser/preferences/preferences.css (preferences/preferences.css)
|
||||
* skin/classic/browser/preferences/in-content/preferences.css (preferences/in-content/preferences.css)
|
||||
skin/classic/browser/preferences/in-content/check.png (preferences/in-content/check.png)
|
||||
skin/classic/browser/preferences/in-content/check@2x.png (preferences/in-content/check@2x.png)
|
||||
skin/classic/browser/preferences/in-content/icons.png (preferences/in-content/icons.png)
|
||||
skin/classic/browser/preferences/in-content/icons@2x.png (preferences/in-content/icons@2x.png)
|
||||
skin/classic/browser/preferences/in-content/header.png (preferences/in-content/icons@2x.png)
|
||||
skin/classic/browser/preferences/in-content/sorter.png (preferences/in-content/sorter.png)
|
||||
skin/classic/browser/preferences/in-content/dropdown.png (preferences/in-content/dropdown.png)
|
||||
skin/classic/browser/preferences/in-content/dropdown-disabled.png (preferences/in-content/dropdown-disabled.png)
|
||||
skin/classic/browser/preferences/in-content/check.png (../shared/incontentprefs/check.png)
|
||||
skin/classic/browser/preferences/in-content/check@2x.png (../shared/incontentprefs/check@2x.png)
|
||||
skin/classic/browser/preferences/in-content/icons.png (../shared/incontentprefs/icons.png)
|
||||
skin/classic/browser/preferences/in-content/icons@2x.png (../shared/incontentprefs/icons@2x.png)
|
||||
skin/classic/browser/preferences/in-content/header.png (../shared/incontentprefs/header.png)
|
||||
skin/classic/browser/preferences/in-content/header@2x.png (../shared/incontentprefs/header@2x.png)
|
||||
skin/classic/browser/preferences/in-content/sorter.png (../shared/incontentprefs/sorter.png)
|
||||
skin/classic/browser/preferences/in-content/sorter@2x.png (../shared/incontentprefs/sorter@2x.png)
|
||||
skin/classic/browser/preferences/in-content/dropdown.png (../shared/incontentprefs/dropdown.png)
|
||||
skin/classic/browser/preferences/in-content/dropdown@2x.png (../shared/incontentprefs/dropdown@2x.png)
|
||||
skin/classic/browser/preferences/in-content/dropdown-disabled.png (../shared/incontentprefs/dropdown-disabled.png)
|
||||
skin/classic/browser/preferences/in-content/dropdown-disabled@2x.png (../shared/incontentprefs/dropdown-disabled@2x.png)
|
||||
skin/classic/browser/preferences/applications.css (preferences/applications.css)
|
||||
skin/classic/browser/preferences/aboutPermissions.css (preferences/aboutPermissions.css)
|
||||
skin/classic/browser/social/services-16.png (social/services-16.png)
|
||||
@ -511,14 +515,18 @@ browser.jar:
|
||||
skin/classic/aero/browser/preferences/saveFile.png (preferences/saveFile-aero.png)
|
||||
* skin/classic/aero/browser/preferences/preferences.css (preferences/preferences.css)
|
||||
* skin/classic/aero/browser/preferences/in-content/preferences.css (preferences/in-content/preferences.css)
|
||||
skin/classic/aero/browser/preferences/in-content/check.png (preferences/in-content/check.png)
|
||||
skin/classic/aero/browser/preferences/in-content/check@2x.png (preferences/in-content/check@2x.png)
|
||||
skin/classic/aero/browser/preferences/in-content/icons.png (preferences/in-content/icons.png)
|
||||
skin/classic/aero/browser/preferences/in-content/icons@2x.png (preferences/in-content/icons@2x.png)
|
||||
skin/classic/aero/browser/preferences/in-content/header.png (preferences/in-content/icons@2x.png)
|
||||
skin/classic/aero/browser/preferences/in-content/sorter.png (preferences/in-content/sorter.png)
|
||||
skin/classic/aero/browser/preferences/in-content/dropdown.png (preferences/in-content/dropdown.png)
|
||||
skin/classic/aero/browser/preferences/in-content/dropdown-disabled.png (preferences/in-content/dropdown-disabled.png)
|
||||
skin/classic/aero/browser/preferences/in-content/check.png (../shared/incontentprefs/check.png)
|
||||
skin/classic/aero/browser/preferences/in-content/check@2x.png (../shared/incontentprefs/check@2x.png)
|
||||
skin/classic/aero/browser/preferences/in-content/icons.png (../shared/incontentprefs/icons.png)
|
||||
skin/classic/aero/browser/preferences/in-content/icons@2x.png (../shared/incontentprefs/icons@2x.png)
|
||||
skin/classic/aero/browser/preferences/in-content/header.png (../shared/incontentprefs/header.png)
|
||||
skin/classic/aero/browser/preferences/in-content/header@2x.png (../shared/incontentprefs/header@2x.png)
|
||||
skin/classic/aero/browser/preferences/in-content/sorter.png (../shared/incontentprefs/sorter.png)
|
||||
skin/classic/aero/browser/preferences/in-content/sorter@2x.png (../shared/incontentprefs/sorter@2x.png)
|
||||
skin/classic/aero/browser/preferences/in-content/dropdown.png (../shared/incontentprefs/dropdown.png)
|
||||
skin/classic/aero/browser/preferences/in-content/dropdown@2x.png (../shared/incontentprefs/dropdown@2x.png)
|
||||
skin/classic/aero/browser/preferences/in-content/dropdown-disabled.png (../shared/incontentprefs/dropdown-disabled.png)
|
||||
skin/classic/aero/browser/preferences/in-content/dropdown-disabled@2x.png (../shared/incontentprefs/dropdown-disabled@2x.png)
|
||||
skin/classic/aero/browser/preferences/applications.css (preferences/applications.css)
|
||||
skin/classic/aero/browser/preferences/aboutPermissions.css (preferences/aboutPermissions.css)
|
||||
skin/classic/aero/browser/social/services-16.png (social/services-16.png)
|
||||
|
Before Width: | Height: | Size: 593 B |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 250 B |
Before Width: | Height: | Size: 250 B |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 30 KiB |
@ -2,7 +2,7 @@
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
- You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
%include ../../../shared/in-content/preferences.css
|
||||
%include ../../../shared/incontentprefs/preferences.css
|
||||
|
||||
menulist:not([editable="true"]) > .menulist-dropmarker {
|
||||
margin-top: 1px;
|
||||
@ -30,75 +30,3 @@ radio[selected]::before {
|
||||
.actionsMenu > .menulist-label-box > .menulist-icon {
|
||||
-moz-margin-end: 9px;
|
||||
}
|
||||
|
||||
@media (min-resolution: 2dppx) {
|
||||
checkbox:hover::before,
|
||||
checkbox[checked]::before {
|
||||
background-size: cover;
|
||||
background-image: -moz-image-rect(url("chrome://browser/skin/preferences/in-content/check@2x.png"), 0, 30, 30, 0);
|
||||
}
|
||||
|
||||
checkbox[checked]::before {
|
||||
background-image: -moz-image-rect(url("chrome://browser/skin/preferences/in-content/check@2x.png"), 0, 60, 30, 30);
|
||||
}
|
||||
|
||||
.category-icon {
|
||||
list-style-image: url("chrome://browser/skin/preferences/in-content/icons@2x.png");
|
||||
}
|
||||
|
||||
#category-general > .category-icon {
|
||||
-moz-image-region: rect(0, 48px, 48px, 0);
|
||||
}
|
||||
|
||||
#category-general[selected] > .category-icon {
|
||||
-moz-image-region: rect(48px, 48px, 96px, 0);
|
||||
}
|
||||
|
||||
#category-content > .category-icon {
|
||||
-moz-image-region: rect(0, 96px, 48px, 48px);
|
||||
}
|
||||
|
||||
#category-content[selected] > .category-icon {
|
||||
-moz-image-region: rect(48px, 96px, 96px, 48px);
|
||||
}
|
||||
|
||||
#category-application > .category-icon {
|
||||
-moz-image-region: rect(0, 144px, 48px, 96px);
|
||||
}
|
||||
|
||||
#category-application[selected] > .category-icon {
|
||||
-moz-image-region: rect(48px, 144px, 96px, 96px);
|
||||
}
|
||||
|
||||
#category-privacy > .category-icon {
|
||||
-moz-image-region: rect(0, 192px, 48px, 144px);
|
||||
}
|
||||
|
||||
#category-privacy[selected] > .category-icon {
|
||||
-moz-image-region: rect(48px, 192px, 96px, 144px);
|
||||
}
|
||||
|
||||
#category-security > .category-icon {
|
||||
-moz-image-region: rect(0, 240px, 48px, 192px);
|
||||
}
|
||||
|
||||
#category-security[selected] > .category-icon {
|
||||
-moz-image-region: rect(48px, 240px, 96px, 192px);
|
||||
}
|
||||
|
||||
#category-sync > .category-icon {
|
||||
-moz-image-region: rect(0, 288px, 48px, 240px);
|
||||
}
|
||||
|
||||
#category-sync[selected] > .category-icon {
|
||||
-moz-image-region: rect(48px, 288px, 96px, 240px);
|
||||
}
|
||||
|
||||
#category-advanced > .category-icon {
|
||||
-moz-image-region: rect(0, 336px, 48px, 288px);
|
||||
}
|
||||
|
||||
#category-advanced[selected] > .category-icon {
|
||||
-moz-image-region: rect(48px, 336px, 96px, 288px);
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 154 B |
@ -159,4 +159,8 @@ label.small {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#fxa-pweng-help-link > image {
|
||||
list-style-image: url("chrome://global/skin/icons/question-16.png");
|
||||
}
|
||||
|
||||
%endif
|
||||
|
@ -2485,6 +2485,8 @@ XULDocument::PrepareToWalk()
|
||||
// Block onload until we've finished building the complete
|
||||
// document content model.
|
||||
BlockOnload();
|
||||
|
||||
nsContentSink::NotifyDocElementCreated(this);
|
||||
}
|
||||
|
||||
// There'd better not be anything on the context stack at this
|
||||
|
@ -5,6 +5,7 @@ support-files =
|
||||
overlay2_bug335375.xul
|
||||
window_bug583948.xul
|
||||
window_bug757137.xul
|
||||
window_documentnotification.xul
|
||||
|
||||
[test_bug199692.xul]
|
||||
[test_bug311681.xul]
|
||||
@ -20,3 +21,4 @@ support-files =
|
||||
[test_bug583948.xul]
|
||||
[test_bug640158_overlay_persist.xul]
|
||||
[test_bug757137.xul]
|
||||
[test_documentnotification.xul]
|
||||
|
40
content/xul/document/test/test_documentnotification.xul
Normal file
@ -0,0 +1,40 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
|
||||
<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
|
||||
type="text/css"?>
|
||||
|
||||
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||
|
||||
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
|
||||
|
||||
<body xmlns="http://www.w3.org/1999/xhtml">
|
||||
<div id="content" style="display: none"/>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
SimpleTest.waitForExplicitFinish();
|
||||
|
||||
var seenNotification = false;
|
||||
function notify(subject, topic, data) {
|
||||
seenNotification = true;
|
||||
is(topic, "document-element-inserted", "Should be the right notification");
|
||||
is(subject, otherWindow.document, "Should have been notified about the right window");
|
||||
ok(subject.documentElement, "documentElement should be defined");
|
||||
}
|
||||
|
||||
var obs = Components.classes["@mozilla.org/observer-service;1"].
|
||||
getService(Components.interfaces.nsIObserverService)
|
||||
obs.addObserver(notify, "document-element-inserted", false);
|
||||
|
||||
var otherWindow = window.open("window_documentnotification.xul", "_new", "chrome");
|
||||
otherWindow.addEventListener("load", function() {
|
||||
ok(seenNotification, "Should have seen the document-element-inserted")
|
||||
obs.removeObserver(notify, "document-element-inserted");
|
||||
window.close();
|
||||
SimpleTest.waitForFocus(function() {
|
||||
SimpleTest.finish();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</window>
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0"?>
|
||||
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
|
||||
|
||||
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
||||
<label value="window_documentnotification.xul"/>
|
||||
</window>
|
@ -27,7 +27,7 @@ class nsDOMNavigationTiming;
|
||||
[ptr] native nsViewPtr(nsView);
|
||||
[ptr] native nsDOMNavigationTimingPtr(nsDOMNavigationTiming);
|
||||
|
||||
[scriptable, builtinclass, uuid(4fc2512b-87c7-4a37-9fac-cee0f116bfdf)]
|
||||
[scriptable, builtinclass, uuid(0cb321bd-5b38-4586-8fcd-d43b366886fb)]
|
||||
interface nsIContentViewer : nsISupports
|
||||
{
|
||||
|
||||
@ -49,6 +49,11 @@ interface nsIContentViewer : nsISupports
|
||||
*/
|
||||
boolean permitUnload([optional] in boolean aCallerClosesWindow);
|
||||
|
||||
/**
|
||||
* Exposes whether we're blocked in a call to permitUnload.
|
||||
*/
|
||||
readonly attribute boolean inPermitUnload;
|
||||
|
||||
/**
|
||||
* As above, but this passes around the aShouldPrompt argument to keep
|
||||
* track of whether the user has responded to a prompt.
|
||||
|
@ -326,6 +326,17 @@ void CleanupOSFileConstants()
|
||||
#define INT_CONSTANT(name) \
|
||||
{ #name, INT_TO_JSVAL(name) }
|
||||
|
||||
/**
|
||||
* Define a simple read-only property holding an unsigned integer.
|
||||
*
|
||||
* @param name The name of the constant. Used both as the JS name for the
|
||||
* constant and to access its value. Must be defined.
|
||||
*
|
||||
* Produces a |ConstantSpec|.
|
||||
*/
|
||||
#define UINT_CONSTANT(name) \
|
||||
{ #name, UINT_TO_JSVAL((name)) }
|
||||
|
||||
/**
|
||||
* End marker for ConstantSpec
|
||||
*/
|
||||
@ -682,7 +693,7 @@ static const dom::ConstantSpec gWinProperties[] =
|
||||
INT_CONSTANT(FILE_END),
|
||||
|
||||
// SetFilePointer error constant
|
||||
INT_CONSTANT(INVALID_SET_FILE_POINTER),
|
||||
UINT_CONSTANT(INVALID_SET_FILE_POINTER),
|
||||
|
||||
// File attributes
|
||||
INT_CONSTANT(FILE_ATTRIBUTE_DIRECTORY),
|
||||
|
@ -23,6 +23,7 @@
|
||||
#include "nsStyleSet.h"
|
||||
#include "nsCSSStyleSheet.h"
|
||||
#include "nsIFrame.h"
|
||||
#include "nsIWritablePropertyBag2.h"
|
||||
#include "nsSubDocumentFrame.h"
|
||||
|
||||
#include "nsILinkHandler.h"
|
||||
@ -1158,6 +1159,14 @@ nsDocumentViewer::PermitUnloadInternal(bool aCallerClosesWindow,
|
||||
nsCOMPtr<nsIPrompt> prompt = do_GetInterface(docShell);
|
||||
|
||||
if (prompt) {
|
||||
nsCOMPtr<nsIWritablePropertyBag2> promptBag = do_QueryInterface(prompt);
|
||||
if (promptBag) {
|
||||
bool isTabModalPromptAllowed;
|
||||
GetIsTabModalPromptAllowed(&isTabModalPromptAllowed);
|
||||
promptBag->SetPropertyAsBool(NS_LITERAL_STRING("allowTabModal"),
|
||||
isTabModalPromptAllowed);
|
||||
}
|
||||
|
||||
nsXPIDLString title, message, stayLabel, leaveLabel;
|
||||
rv = nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES,
|
||||
"OnBeforeUnloadTitle",
|
||||
@ -1200,7 +1209,19 @@ nsDocumentViewer::PermitUnloadInternal(bool aCallerClosesWindow,
|
||||
leaveLabel, stayLabel, nullptr, nullptr,
|
||||
&dummy, &buttonPressed);
|
||||
mInPermitUnloadPrompt = false;
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
// If the prompt aborted, we tell our consumer that it is not allowed
|
||||
// to unload the page. One reason that prompts abort is that the user
|
||||
// performed some action that caused the page to unload while our prompt
|
||||
// was active. In those cases we don't want our consumer to also unload
|
||||
// the page.
|
||||
//
|
||||
// XXX: Are there other cases where prompts can abort? Is it ok to
|
||||
// prevent unloading the page in those cases?
|
||||
if (NS_FAILED(rv)) {
|
||||
*aPermitUnload = false;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
// Button 0 == leave, button 1 == stay
|
||||
*aPermitUnload = (buttonPressed == 0);
|
||||
@ -1247,6 +1268,13 @@ nsDocumentViewer::GetBeforeUnloadFiring(bool* aInEvent)
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsDocumentViewer::GetInPermitUnload(bool* aInEvent)
|
||||
{
|
||||
*aInEvent = mInPermitUnloadPrompt;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
NS_IMETHODIMP
|
||||
nsDocumentViewer::ResetCloseWindow()
|
||||
{
|
||||
@ -4313,7 +4341,7 @@ nsDocumentViewer::GetHistoryEntry(nsISHEntry **aHistoryEntry)
|
||||
NS_IMETHODIMP
|
||||
nsDocumentViewer::GetIsTabModalPromptAllowed(bool *aAllowed)
|
||||
{
|
||||
*aAllowed = !(mInPermitUnload || mHidden);
|
||||
*aAllowed = !mHidden;
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
|
@ -13,11 +13,13 @@ import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.Build;
|
||||
import android.support.v4.app.NotificationCompat;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.StyleSpan;
|
||||
|
||||
public class DataReportingNotification {
|
||||
@ -28,15 +30,50 @@ public class DataReportingNotification {
|
||||
|
||||
private static final String PREFS_POLICY_NOTIFIED_TIME = "datareporting.policy.dataSubmissionPolicyNotifiedTime";
|
||||
private static final String PREFS_POLICY_VERSION = "datareporting.policy.dataSubmissionPolicyVersion";
|
||||
private static final int DATA_REPORTING_VERSION = 1;
|
||||
private static final int DATA_REPORTING_VERSION = 2;
|
||||
|
||||
public static void checkAndNotifyPolicy(Context context) {
|
||||
SharedPreferences dataPrefs = GeckoSharedPrefs.forApp(context);
|
||||
final int currentVersion = dataPrefs.getInt(PREFS_POLICY_VERSION, -1);
|
||||
|
||||
// Notify if user has not been notified or if policy version has changed.
|
||||
if ((!dataPrefs.contains(PREFS_POLICY_NOTIFIED_TIME)) ||
|
||||
(DATA_REPORTING_VERSION != dataPrefs.getInt(PREFS_POLICY_VERSION, -1))) {
|
||||
if (currentVersion < 1) {
|
||||
// This is a first run, so notify user about data policy.
|
||||
notifyDataPolicy(context, dataPrefs);
|
||||
|
||||
// If healthreport is enabled, set default preference value.
|
||||
if (AppConstants.MOZ_SERVICES_HEALTHREPORT) {
|
||||
SharedPreferences.Editor editor = dataPrefs.edit();
|
||||
editor.putBoolean(GeckoPreferences.PREFS_HEALTHREPORT_UPLOAD_ENABLED, true);
|
||||
editor.commit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentVersion == 1) {
|
||||
// Redisplay notification only for Beta because version 2 updates Beta policy and update version.
|
||||
if (TextUtils.equals("beta", AppConstants.MOZ_UPDATE_CHANNEL)) {
|
||||
notifyDataPolicy(context, dataPrefs);
|
||||
} else {
|
||||
// Silently update the version.
|
||||
SharedPreferences.Editor editor = dataPrefs.edit();
|
||||
editor.putInt(PREFS_POLICY_VERSION, DATA_REPORTING_VERSION);
|
||||
editor.commit();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentVersion >= DATA_REPORTING_VERSION) {
|
||||
// Do nothing, we're at a current (or future) version.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch a notification of the data policy, and record notification time and version.
|
||||
*/
|
||||
private static void notifyDataPolicy(Context context, SharedPreferences sharedPrefs) {
|
||||
boolean result = false;
|
||||
try {
|
||||
// Launch main App to launch Data choices when notification is clicked.
|
||||
Intent prefIntent = new Intent(GeckoApp.ACTION_LAUNCH_SETTINGS);
|
||||
prefIntent.setClassName(AppConstants.ANDROID_PACKAGE_NAME, AppConstants.BROWSER_INTENT_CLASS_NAME);
|
||||
@ -45,21 +82,22 @@ public class DataReportingNotification {
|
||||
prefIntent.putExtra(ALERT_NAME_DATAREPORTING_NOTIFICATION, true);
|
||||
|
||||
PendingIntent contentIntent = PendingIntent.getActivity(context, 0, prefIntent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
final Resources resources = context.getResources();
|
||||
|
||||
// Create and send notification.
|
||||
String notificationTitle = context.getResources().getString(R.string.datareporting_notification_title);
|
||||
String notificationTitle = resources.getString(R.string.datareporting_notification_title);
|
||||
String notificationSummary;
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN) {
|
||||
notificationSummary = context.getResources().getString(R.string.datareporting_notification_action);
|
||||
notificationSummary = resources.getString(R.string.datareporting_notification_action);
|
||||
} else {
|
||||
// Display partial version of Big Style notification for supporting devices.
|
||||
notificationSummary = context.getResources().getString(R.string.datareporting_notification_summary);
|
||||
// Display partial version of Big Style notification for supporting devices.
|
||||
notificationSummary = resources.getString(R.string.datareporting_notification_summary);
|
||||
}
|
||||
String notificationAction = context.getResources().getString(R.string.datareporting_notification_action);
|
||||
String notificationBigSummary = context.getResources().getString(R.string.datareporting_notification_summary);
|
||||
String notificationAction = resources.getString(R.string.datareporting_notification_action);
|
||||
String notificationBigSummary = resources.getString(R.string.datareporting_notification_summary);
|
||||
|
||||
// Make styled ticker text for display in notification bar.
|
||||
String tickerString = context.getResources().getString(R.string.datareporting_notification_ticker_text);
|
||||
String tickerString = resources.getString(R.string.datareporting_notification_ticker_text);
|
||||
SpannableString tickerText = new SpannableString(tickerString);
|
||||
// Bold the notification title of the ticker text, which is the same string as notificationTitle.
|
||||
tickerText.setSpan(new StyleSpan(Typeface.BOLD), 0, notificationTitle.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
|
||||
@ -81,17 +119,16 @@ public class DataReportingNotification {
|
||||
notificationManager.notify(notificationID, notification);
|
||||
|
||||
// Record version and notification time.
|
||||
SharedPreferences.Editor editor = dataPrefs.edit();
|
||||
SharedPreferences.Editor editor = sharedPrefs.edit();
|
||||
long now = System.currentTimeMillis();
|
||||
editor.putLong(PREFS_POLICY_NOTIFIED_TIME, now);
|
||||
editor.putInt(PREFS_POLICY_VERSION, DATA_REPORTING_VERSION);
|
||||
|
||||
// If healthreport is enabled, set default preference value.
|
||||
if (AppConstants.MOZ_SERVICES_HEALTHREPORT) {
|
||||
editor.putBoolean(GeckoPreferences.PREFS_HEALTHREPORT_UPLOAD_ENABLED, true);
|
||||
}
|
||||
|
||||
editor.commit();
|
||||
result = true;
|
||||
} finally {
|
||||
// We want to track any errors, so record notification outcome.
|
||||
final String notificationEvent = TelemetryContract.Event.POLICY_NOTIFICATION_SUCCESS + result;
|
||||
Telemetry.sendUIEvent(notificationEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -14,7 +14,10 @@ public interface TelemetryContract {
|
||||
* Holds event names. Intended for use with
|
||||
* Telemetry.sendUIEvent() as the "action" parameter.
|
||||
*/
|
||||
public interface Event {}
|
||||
public interface Event {
|
||||
// Outcome of data policy notification: can be true or false.
|
||||
public static final String POLICY_NOTIFICATION_SUCCESS = "policynotification.success.1:";
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds event methods. Intended for use in
|
||||
|
@ -9,7 +9,7 @@ import org.mozilla.gecko.sync.ExtendedJSONObject;
|
||||
import org.mozilla.gecko.sync.Utils;
|
||||
|
||||
public abstract class State {
|
||||
public static final long CURRENT_VERSION = 1L;
|
||||
public static final long CURRENT_VERSION = 2L;
|
||||
|
||||
public enum StateLabel {
|
||||
Engaged,
|
||||
|
@ -7,18 +7,65 @@ package org.mozilla.gecko.fxa.login;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
|
||||
import org.mozilla.gecko.background.common.log.Logger;
|
||||
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
|
||||
import org.mozilla.gecko.browserid.DSACryptoImplementation;
|
||||
import org.mozilla.gecko.browserid.RSACryptoImplementation;
|
||||
import org.mozilla.gecko.fxa.FxAccountConstants;
|
||||
import org.mozilla.gecko.fxa.login.State.StateLabel;
|
||||
import org.mozilla.gecko.sync.ExtendedJSONObject;
|
||||
import org.mozilla.gecko.sync.NonObjectJSONException;
|
||||
import org.mozilla.gecko.sync.Utils;
|
||||
|
||||
/**
|
||||
* Create {@link State} instances from serialized representations.
|
||||
* <p>
|
||||
* Version 1 recognizes 5 state labels (Engaged, Cohabiting, Married, Separated,
|
||||
* Doghouse). In the Cohabiting and Married states, the associated key pairs are
|
||||
* always RSA key pairs.
|
||||
* <p>
|
||||
* Version 2 is identical to version 1, except that in the Cohabiting and
|
||||
* Married states, the associated keypairs are always DSA key pairs.
|
||||
*/
|
||||
public class StateFactory {
|
||||
private static final String LOG_TAG = StateFactory.class.getSimpleName();
|
||||
|
||||
private static final int KEY_PAIR_SIZE_IN_BITS_V1 = 1024;
|
||||
|
||||
public static BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException {
|
||||
// New key pairs are always DSA.
|
||||
return DSACryptoImplementation.generateKeyPair(KEY_PAIR_SIZE_IN_BITS_V1);
|
||||
}
|
||||
|
||||
protected static BrowserIDKeyPair keyPairFromJSONObjectV1(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
|
||||
// V1 key pairs are RSA.
|
||||
return RSACryptoImplementation.fromJSONObject(o);
|
||||
}
|
||||
|
||||
protected static BrowserIDKeyPair keyPairFromJSONObjectV2(ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException {
|
||||
// V2 key pairs are DSA.
|
||||
return DSACryptoImplementation.fromJSONObject(o);
|
||||
}
|
||||
|
||||
public static State fromJSONObject(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
|
||||
Long version = o.getLong("version");
|
||||
if (version == null || version.intValue() != 1) {
|
||||
throw new IllegalStateException("version must be 1");
|
||||
if (version == null) {
|
||||
throw new IllegalStateException("version must not be null");
|
||||
}
|
||||
|
||||
final int v = version.intValue();
|
||||
if (v == 2) {
|
||||
// The most common case is the most recent version.
|
||||
return fromJSONObjectV2(stateLabel, o);
|
||||
}
|
||||
if (v == 1) {
|
||||
final State state = fromJSONObjectV1(stateLabel, o);
|
||||
return migrateV1toV2(stateLabel, state);
|
||||
}
|
||||
throw new IllegalStateException("version must be in {1, 2}");
|
||||
}
|
||||
|
||||
protected static State fromJSONObjectV1(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
|
||||
switch (stateLabel) {
|
||||
case Engaged:
|
||||
return new Engaged(
|
||||
@ -35,7 +82,7 @@ public class StateFactory {
|
||||
Utils.hex2Byte(o.getString("sessionToken")),
|
||||
Utils.hex2Byte(o.getString("kA")),
|
||||
Utils.hex2Byte(o.getString("kB")),
|
||||
RSACryptoImplementation.fromJSONObject(o.getObject("keyPair")));
|
||||
keyPairFromJSONObjectV1(o.getObject("keyPair")));
|
||||
case Married:
|
||||
return new Married(
|
||||
o.getString("email"),
|
||||
@ -43,7 +90,7 @@ public class StateFactory {
|
||||
Utils.hex2Byte(o.getString("sessionToken")),
|
||||
Utils.hex2Byte(o.getString("kA")),
|
||||
Utils.hex2Byte(o.getString("kB")),
|
||||
RSACryptoImplementation.fromJSONObject(o.getObject("keyPair")),
|
||||
keyPairFromJSONObjectV1(o.getObject("keyPair")),
|
||||
o.getString("certificate"));
|
||||
case Separated:
|
||||
return new Separated(
|
||||
@ -59,4 +106,81 @@ public class StateFactory {
|
||||
throw new IllegalStateException("unrecognized state label: " + stateLabel);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exactly the same as {@link fromJSONObjectV1}, except that all key pairs are DSA key pairs.
|
||||
*/
|
||||
protected static State fromJSONObjectV2(StateLabel stateLabel, ExtendedJSONObject o) throws InvalidKeySpecException, NoSuchAlgorithmException, NonObjectJSONException {
|
||||
switch (stateLabel) {
|
||||
case Cohabiting:
|
||||
return new Cohabiting(
|
||||
o.getString("email"),
|
||||
o.getString("uid"),
|
||||
Utils.hex2Byte(o.getString("sessionToken")),
|
||||
Utils.hex2Byte(o.getString("kA")),
|
||||
Utils.hex2Byte(o.getString("kB")),
|
||||
keyPairFromJSONObjectV2(o.getObject("keyPair")));
|
||||
case Married:
|
||||
return new Married(
|
||||
o.getString("email"),
|
||||
o.getString("uid"),
|
||||
Utils.hex2Byte(o.getString("sessionToken")),
|
||||
Utils.hex2Byte(o.getString("kA")),
|
||||
Utils.hex2Byte(o.getString("kB")),
|
||||
keyPairFromJSONObjectV2(o.getObject("keyPair")),
|
||||
o.getString("certificate"));
|
||||
default:
|
||||
return fromJSONObjectV1(stateLabel, o);
|
||||
}
|
||||
}
|
||||
|
||||
protected static void logMigration(State from, State to) {
|
||||
if (!FxAccountConstants.LOG_PERSONAL_INFORMATION) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
FxAccountConstants.pii(LOG_TAG, "V1 persisted state is: " + from.toJSONObject().toJSONString());
|
||||
} catch (Exception e) {
|
||||
Logger.warn(LOG_TAG, "Error producing JSON representation of V1 state.", e);
|
||||
}
|
||||
FxAccountConstants.pii(LOG_TAG, "Generated new V2 state: " + to.toJSONObject().toJSONString());
|
||||
}
|
||||
|
||||
protected static State migrateV1toV2(StateLabel stateLabel, State state) throws NoSuchAlgorithmException {
|
||||
if (state == null) {
|
||||
// This should never happen, but let's be careful.
|
||||
Logger.error(LOG_TAG, "Got null state in migrateV1toV2; returning null.");
|
||||
return state;
|
||||
}
|
||||
|
||||
Logger.info(LOG_TAG, "Migrating V1 persisted State to V2; stateLabel: " + stateLabel);
|
||||
|
||||
// In V1, we use an RSA keyPair. In V2, we use a DSA keyPair. Only
|
||||
// Cohabiting and Married states have a persisted keyPair at all; all
|
||||
// other states need no conversion at all.
|
||||
switch (stateLabel) {
|
||||
case Cohabiting: {
|
||||
// In the Cohabiting state, we can just generate a new key pair and move on.
|
||||
final Cohabiting cohabiting = (Cohabiting) state;
|
||||
final BrowserIDKeyPair keyPair = generateKeyPair();
|
||||
final State migrated = new Cohabiting(cohabiting.email, cohabiting.uid, cohabiting.sessionToken, cohabiting.kA, cohabiting.kB, keyPair);
|
||||
logMigration(cohabiting, migrated);
|
||||
return migrated;
|
||||
}
|
||||
case Married: {
|
||||
// In the Married state, we cannot only change the key pair: the stored
|
||||
// certificate signs the public key of the now obsolete key pair. We
|
||||
// regress to the Cohabiting state; the next time we sync, we should
|
||||
// advance back to Married.
|
||||
final Married married = (Married) state;
|
||||
final BrowserIDKeyPair keyPair = generateKeyPair();
|
||||
final State migrated = new Cohabiting(married.email, married.uid, married.sessionToken, married.kA, married.kB, keyPair);
|
||||
logMigration(married, migrated);
|
||||
return migrated;
|
||||
}
|
||||
default:
|
||||
// Otherwise, V1 and V2 states are identical.
|
||||
return state;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -20,7 +20,6 @@ import org.mozilla.gecko.background.fxa.FxAccountClient20;
|
||||
import org.mozilla.gecko.background.fxa.SkewHandler;
|
||||
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
|
||||
import org.mozilla.gecko.browserid.JSONWebTokenUtils;
|
||||
import org.mozilla.gecko.browserid.RSACryptoImplementation;
|
||||
import org.mozilla.gecko.browserid.verifier.BrowserIDRemoteVerifierClient;
|
||||
import org.mozilla.gecko.browserid.verifier.BrowserIDVerifierDelegate;
|
||||
import org.mozilla.gecko.fxa.FirefoxAccounts;
|
||||
@ -34,6 +33,7 @@ import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
|
||||
import org.mozilla.gecko.fxa.login.Married;
|
||||
import org.mozilla.gecko.fxa.login.State;
|
||||
import org.mozilla.gecko.fxa.login.State.StateLabel;
|
||||
import org.mozilla.gecko.fxa.login.StateFactory;
|
||||
import org.mozilla.gecko.sync.BackoffHandler;
|
||||
import org.mozilla.gecko.sync.ExtendedJSONObject;
|
||||
import org.mozilla.gecko.sync.GlobalSession;
|
||||
@ -537,7 +537,7 @@ public class FxAccountSyncAdapter extends AbstractThreadedSyncAdapter {
|
||||
|
||||
@Override
|
||||
public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException {
|
||||
return RSACryptoImplementation.generateKeyPair(1024);
|
||||
return StateFactory.generateKeyPair();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -23,7 +23,6 @@ import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.graphics.Point;
|
||||
import android.graphics.PointF;
|
||||
import android.graphics.Rect;
|
||||
@ -325,7 +324,6 @@ public class LayerView extends FrameLayout implements Tabs.OnTabsChangedListener
|
||||
|
||||
SurfaceHolder holder = mSurfaceView.getHolder();
|
||||
holder.addCallback(new SurfaceListener());
|
||||
holder.setFormat(PixelFormat.RGB_565);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,7 @@ import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.util.Log;
|
||||
@ -86,6 +87,11 @@ abstract class HomeFragment extends Fragment {
|
||||
|
||||
menu.setHeaderTitle(info.getDisplayTitle());
|
||||
|
||||
// Hide ununsed menu items.
|
||||
menu.findItem(R.id.top_sites_edit).setVisible(false);
|
||||
menu.findItem(R.id.top_sites_pin).setVisible(false);
|
||||
menu.findItem(R.id.top_sites_unpin).setVisible(false);
|
||||
|
||||
// Hide the "Edit" menuitem if this item isn't a bookmark,
|
||||
// or if this is a reading list item.
|
||||
if (!info.hasBookmarkId() || info.isInReadingList()) {
|
||||
@ -152,7 +158,10 @@ abstract class HomeFragment extends Fragment {
|
||||
flags |= Tabs.LOADURL_PRIVATE;
|
||||
|
||||
final String url = (info.isInReadingList() ? ReaderModeUtils.getAboutReaderForUrl(info.url) : info.url);
|
||||
Tabs.getInstance().loadUrl(url, flags);
|
||||
|
||||
// Some pinned site items have "user-entered" urls. URLs entered in the PinSiteDialog are wrapped in
|
||||
// a special URI until we can get a valid URL. If the url is a user-entered url, decode the URL before loading it.
|
||||
Tabs.getInstance().loadUrl(decodeUserEnteredUrl(url), flags);
|
||||
Toast.makeText(context, R.string.new_tab_opened, Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
}
|
||||
@ -218,6 +227,23 @@ abstract class HomeFragment extends Fragment {
|
||||
return mCanLoadHint;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a url with a user-entered scheme, extract the
|
||||
* scheme-specific component. For e.g, given "user-entered://www.google.com",
|
||||
* this method returns "//www.google.com". If the passed url
|
||||
* does not have a user-entered scheme, the same url will be returned.
|
||||
*
|
||||
* @param url to be decoded
|
||||
* @return url component entered by user
|
||||
*/
|
||||
public static String decodeUserEnteredUrl(String url) {
|
||||
Uri uri = Uri.parse(url);
|
||||
if ("user-entered".equals(uri.getScheme())) {
|
||||
return uri.getSchemeSpecificPart();
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
protected abstract void load();
|
||||
|
||||
protected boolean canLoad() {
|
||||
|
@ -215,13 +215,13 @@ public class ReadingListPanel extends HomeFragment {
|
||||
|
||||
@Override
|
||||
public void bindView(View view, Context context, Cursor cursor) {
|
||||
final TwoLinePageRow row = (TwoLinePageRow) view;
|
||||
final ReadingListRow row = (ReadingListRow) view;
|
||||
row.updateFromCursor(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View newView(Context context, Cursor cursor, ViewGroup parent) {
|
||||
return LayoutInflater.from(parent.getContext()).inflate(R.layout.bookmark_item_row, parent, false);
|
||||
return LayoutInflater.from(parent.getContext()).inflate(R.layout.reading_list_item_row, parent, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
43
mobile/android/base/home/ReadingListRow.java
Normal file
@ -0,0 +1,43 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* 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/. */
|
||||
|
||||
package org.mozilla.gecko.home;
|
||||
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.Tab;
|
||||
import org.mozilla.gecko.Tabs;
|
||||
import org.mozilla.gecko.AboutPages;
|
||||
import org.mozilla.gecko.home.TwoLinePageRow;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
public class ReadingListRow extends TwoLinePageRow {
|
||||
|
||||
public ReadingListRow(Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public ReadingListRow(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateDisplayedUrl() {
|
||||
String pageUrl = getUrl();
|
||||
|
||||
boolean isPrivate = Tabs.getInstance().getSelectedTab().isPrivate();
|
||||
Tab tab = Tabs.getInstance().getFirstTabForUrl(pageUrl, isPrivate);
|
||||
|
||||
if (tab != null && AboutPages.isAboutReader(tab.getURL())) {
|
||||
setUrl(R.string.switch_to_tab);
|
||||
setSwitchToTabIcon(R.drawable.ic_url_bar_tab);
|
||||
} else {
|
||||
setUrl(pageUrl);
|
||||
setSwitchToTabIcon(NO_ICON);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -12,7 +12,6 @@ import org.mozilla.gecko.ThumbnailHelper;
|
||||
import org.mozilla.gecko.db.BrowserDB.TopSitesCursorWrapper;
|
||||
import org.mozilla.gecko.db.BrowserDB.URLColumns;
|
||||
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
|
||||
import org.mozilla.gecko.util.StringUtils;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
@ -105,7 +104,7 @@ public class TopSitesGridView extends GridView {
|
||||
TopSitesGridItemView row = (TopSitesGridItemView) view;
|
||||
|
||||
// Decode "user-entered" URLs before loading them.
|
||||
String url = TopSitesPanel.decodeUserEnteredUrl(row.getUrl());
|
||||
String url = HomeFragment.decodeUserEnteredUrl(row.getUrl());
|
||||
|
||||
// If the url is empty, the user can pin a site.
|
||||
// If not, navigate to the page given by the url.
|
||||
@ -125,7 +124,14 @@ public class TopSitesGridView extends GridView {
|
||||
@Override
|
||||
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
|
||||
Cursor cursor = (Cursor) parent.getItemAtPosition(position);
|
||||
mContextMenuInfo = new TopSitesGridContextMenuInfo(view, position, id, cursor);
|
||||
|
||||
if (cursor == null) {
|
||||
mContextMenuInfo = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
mContextMenuInfo = new TopSitesGridContextMenuInfo(view, position, id);
|
||||
updateContextMenuFromCursor(mContextMenuInfo, cursor);
|
||||
return showContextMenuForChild(TopSitesGridView.this);
|
||||
}
|
||||
});
|
||||
@ -221,6 +227,18 @@ public class TopSitesGridView extends GridView {
|
||||
return mContextMenuInfo;
|
||||
}
|
||||
|
||||
/*
|
||||
* Update the fields of a TopSitesGridContextMenuInfo object
|
||||
* from a cursor.
|
||||
*
|
||||
* @param info context menu info object to be updated
|
||||
* @param cursor used to update the context menu info object
|
||||
*/
|
||||
private void updateContextMenuFromCursor(TopSitesGridContextMenuInfo info, Cursor cursor) {
|
||||
info.url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL));
|
||||
info.title = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.TITLE));
|
||||
info.isPinned = ((TopSitesCursorWrapper) cursor).isPinned();
|
||||
}
|
||||
/**
|
||||
* Set an url open listener to be used by this view.
|
||||
*
|
||||
@ -240,29 +258,13 @@ public class TopSitesGridView extends GridView {
|
||||
}
|
||||
|
||||
/**
|
||||
* A ContextMenuInfo for TopBoomarksView that adds details from the cursor.
|
||||
* Stores information regarding the creation of the context menu for a GridView item.
|
||||
*/
|
||||
public static class TopSitesGridContextMenuInfo extends AdapterContextMenuInfo {
|
||||
public static class TopSitesGridContextMenuInfo extends HomeContextMenuInfo {
|
||||
public boolean isPinned = false;
|
||||
|
||||
public String url;
|
||||
public String title;
|
||||
public boolean isPinned;
|
||||
|
||||
public TopSitesGridContextMenuInfo(View targetView, int position, long id, Cursor cursor) {
|
||||
public TopSitesGridContextMenuInfo(View targetView, int position, long id) {
|
||||
super(targetView, position, id);
|
||||
|
||||
if (cursor == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
url = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.URL));
|
||||
title = cursor.getString(cursor.getColumnIndexOrThrow(URLColumns.TITLE));
|
||||
isPinned = ((TopSitesCursorWrapper) cursor).isPinned();
|
||||
}
|
||||
|
||||
public String getDisplayTitle() {
|
||||
return TextUtils.isEmpty(title) ?
|
||||
StringUtils.stripCommonSubdomains(StringUtils.stripScheme(url, StringUtils.UrlFlags.STRIP_HTTPS)) : title;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,9 +10,7 @@ import java.util.EnumSet;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.mozilla.gecko.GeckoAppShell;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.Tabs;
|
||||
import org.mozilla.gecko.db.BrowserContract.Combined;
|
||||
import org.mozilla.gecko.db.BrowserContract.Thumbnails;
|
||||
import org.mozilla.gecko.db.BrowserDB;
|
||||
@ -30,7 +28,6 @@ import org.mozilla.gecko.util.ThreadUtils;
|
||||
import android.app.Activity;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
@ -52,7 +49,6 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.Toast;
|
||||
|
||||
/**
|
||||
* Fragment that displays frecency search results in a ListView.
|
||||
@ -267,17 +263,21 @@ public class TopSitesPanel extends HomeFragment {
|
||||
return;
|
||||
}
|
||||
|
||||
// HomeFragment will handle the default case.
|
||||
if (menuInfo instanceof HomeContextMenuInfo) {
|
||||
super.onCreateContextMenu(menu, view, menuInfo);
|
||||
}
|
||||
|
||||
if (!(menuInfo instanceof TopSitesGridContextMenuInfo)) {
|
||||
// Long pressed item was not a Top Sites GridView item. Superclass
|
||||
// can handle this.
|
||||
super.onCreateContextMenu(menu, view, menuInfo);
|
||||
return;
|
||||
}
|
||||
|
||||
// Long pressed item was a Top Sites GridView item, handle it.
|
||||
MenuInflater inflater = new MenuInflater(view.getContext());
|
||||
inflater.inflate(R.menu.top_sites_contextmenu, menu);
|
||||
inflater.inflate(R.menu.home_contextmenu, menu);
|
||||
|
||||
// Hide ununsed menu items.
|
||||
menu.findItem(R.id.home_open_in_reader).setVisible(false);
|
||||
menu.findItem(R.id.home_edit_bookmark).setVisible(false);
|
||||
menu.findItem(R.id.home_remove).setVisible(false);
|
||||
|
||||
TopSitesGridContextMenuInfo info = (TopSitesGridContextMenuInfo) menuInfo;
|
||||
menu.setHeaderTitle(info.getDisplayTitle());
|
||||
@ -289,8 +289,8 @@ public class TopSitesPanel extends HomeFragment {
|
||||
menu.findItem(R.id.top_sites_unpin).setVisible(false);
|
||||
}
|
||||
} else {
|
||||
menu.findItem(R.id.top_sites_open_new_tab).setVisible(false);
|
||||
menu.findItem(R.id.top_sites_open_private_tab).setVisible(false);
|
||||
menu.findItem(R.id.home_open_new_tab).setVisible(false);
|
||||
menu.findItem(R.id.home_open_private_tab).setVisible(false);
|
||||
menu.findItem(R.id.top_sites_pin).setVisible(false);
|
||||
menu.findItem(R.id.top_sites_unpin).setVisible(false);
|
||||
}
|
||||
@ -298,9 +298,13 @@ public class TopSitesPanel extends HomeFragment {
|
||||
|
||||
@Override
|
||||
public boolean onContextItemSelected(MenuItem item) {
|
||||
if (super.onContextItemSelected(item)) {
|
||||
// HomeFragment was able to handle to selected item.
|
||||
return true;
|
||||
}
|
||||
|
||||
ContextMenuInfo menuInfo = item.getMenuInfo();
|
||||
|
||||
// HomeFragment will handle the default case.
|
||||
if (menuInfo == null || !(menuInfo instanceof TopSitesGridContextMenuInfo)) {
|
||||
return false;
|
||||
}
|
||||
@ -309,21 +313,6 @@ public class TopSitesPanel extends HomeFragment {
|
||||
final Activity activity = getActivity();
|
||||
|
||||
final int itemId = item.getItemId();
|
||||
if (itemId == R.id.top_sites_open_new_tab || itemId == R.id.top_sites_open_private_tab) {
|
||||
if (info.url == null) {
|
||||
Log.e(LOGTAG, "Can't open in new tab because URL is null");
|
||||
return false;
|
||||
}
|
||||
|
||||
int flags = Tabs.LOADURL_NEW_TAB | Tabs.LOADURL_BACKGROUND;
|
||||
if (item.getItemId() == R.id.top_sites_open_private_tab)
|
||||
flags |= Tabs.LOADURL_PRIVATE;
|
||||
|
||||
// Decode "user-entered" URLs before loading them.
|
||||
Tabs.getInstance().loadUrl(decodeUserEnteredUrl(info.url), flags);
|
||||
Toast.makeText(activity, R.string.new_tab_opened, Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (itemId == R.id.top_sites_pin) {
|
||||
final String url = info.url;
|
||||
@ -361,28 +350,6 @@ public class TopSitesPanel extends HomeFragment {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (itemId == R.id.home_share) {
|
||||
if (info.url == null) {
|
||||
Log.w(LOGTAG, "Share not enabled for context menu because URL is null.");
|
||||
return false;
|
||||
} else {
|
||||
GeckoAppShell.openUriExternal(info.url, SHARE_MIME_TYPE, "", "",
|
||||
Intent.ACTION_SEND, info.getDisplayTitle());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (itemId == R.id.home_add_to_launcher) {
|
||||
if (info.url == null) {
|
||||
Log.w(LOGTAG, "Not enabling 'Add to home page' because URL is null.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Fetch an icon big enough for use as a home screen icon.
|
||||
Favicons.getPreferredSizeFaviconForPage(info.url, new GeckoAppShell.CreateShortcutFaviconLoadedListener(info.url, info.getDisplayTitle()));
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -403,14 +370,6 @@ public class TopSitesPanel extends HomeFragment {
|
||||
return Uri.fromParts("user-entered", url, null).toString();
|
||||
}
|
||||
|
||||
static String decodeUserEnteredUrl(String url) {
|
||||
Uri uri = Uri.parse(url);
|
||||
if ("user-entered".equals(uri.getScheme())) {
|
||||
return uri.getSchemeSpecificPart();
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Listener for editing pinned sites.
|
||||
*/
|
||||
|
@ -30,7 +30,7 @@ import android.widget.TextView;
|
||||
public class TwoLinePageRow extends LinearLayout
|
||||
implements Tabs.OnTabsChangedListener {
|
||||
|
||||
private static final int NO_ICON = 0;
|
||||
protected static final int NO_ICON = 0;
|
||||
|
||||
private final TextView mTitle;
|
||||
private final TextView mUrl;
|
||||
@ -122,15 +122,19 @@ public class TwoLinePageRow extends LinearLayout
|
||||
mTitle.setText(text);
|
||||
}
|
||||
|
||||
private void setUrl(String text) {
|
||||
protected void setUrl(String text) {
|
||||
mUrl.setText(text);
|
||||
}
|
||||
|
||||
private void setUrl(int stringId) {
|
||||
protected void setUrl(int stringId) {
|
||||
mUrl.setText(stringId);
|
||||
}
|
||||
|
||||
private void setSwitchToTabIcon(int iconId) {
|
||||
protected String getUrl() {
|
||||
return mPageUrl;
|
||||
}
|
||||
|
||||
protected void setSwitchToTabIcon(int iconId) {
|
||||
if (mSwitchToTabIconId == iconId) {
|
||||
return;
|
||||
}
|
||||
@ -159,10 +163,10 @@ public class TwoLinePageRow extends LinearLayout
|
||||
|
||||
/**
|
||||
* Replaces the page URL with "Switch to tab" if there is already a tab open with that URL.
|
||||
* Only looks for tabs that are either private or non-private, depending on the current
|
||||
* Only looks for tabs that are either private or non-private, depending on the current
|
||||
* selected tab.
|
||||
*/
|
||||
private void updateDisplayedUrl() {
|
||||
protected void updateDisplayedUrl() {
|
||||
boolean isPrivate = Tabs.getInstance().getSelectedTab().isPrivate();
|
||||
Tab tab = Tabs.getInstance().getFirstTabForUrl(mPageUrl, isPrivate);
|
||||
if (!mShowIcons || tab == null) {
|
||||
|
@ -268,6 +268,7 @@ gbjar.sources += [
|
||||
'home/PanelViewItemHandler.java',
|
||||
'home/PinSiteDialog.java',
|
||||
'home/ReadingListPanel.java',
|
||||
'home/ReadingListRow.java',
|
||||
'home/SearchEngine.java',
|
||||
'home/SearchEngineRow.java',
|
||||
'home/SearchLoader.java',
|
||||
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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/. -->
|
||||
|
||||
<org.mozilla.gecko.home.ReadingListRow xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
style="@style/Widget.BookmarkItemView"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="@dimen/page_row_height"
|
||||
android:minHeight="@dimen/page_row_height"/>
|
@ -17,6 +17,15 @@
|
||||
<item android:id="@+id/home_share"
|
||||
android:title="@string/contextmenu_share"/>
|
||||
|
||||
<item android:id="@+id/top_sites_edit"
|
||||
android:title="@string/contextmenu_top_sites_edit"/>
|
||||
|
||||
<item android:id="@+id/top_sites_pin"
|
||||
android:title="@string/contextmenu_top_sites_pin"/>
|
||||
|
||||
<item android:id="@+id/top_sites_unpin"
|
||||
android:title="@string/contextmenu_top_sites_unpin"/>
|
||||
|
||||
<item android:id="@+id/home_edit_bookmark"
|
||||
android:title="@string/contextmenu_edit_bookmark"/>
|
||||
|
||||
|
@ -1,28 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- 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/. -->
|
||||
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item android:id="@+id/top_sites_open_new_tab"
|
||||
android:title="@string/contextmenu_open_new_tab"/>
|
||||
|
||||
<item android:id="@+id/top_sites_open_private_tab"
|
||||
android:title="@string/contextmenu_open_private_tab"/>
|
||||
|
||||
<item android:id="@+id/home_share"
|
||||
android:title="@string/contextmenu_share"/>
|
||||
|
||||
<item android:id="@+id/top_sites_edit"
|
||||
android:title="@string/contextmenu_top_sites_edit"/>
|
||||
|
||||
<item android:id="@+id/top_sites_pin"
|
||||
android:title="@string/contextmenu_top_sites_pin"/>
|
||||
|
||||
<item android:id="@+id/top_sites_unpin"
|
||||
android:title="@string/contextmenu_top_sites_unpin"/>
|
||||
|
||||
<item android:id="@+id/home_add_to_launcher"
|
||||
android:title="@string/contextmenu_add_to_launcher"/>
|
||||
</menu>
|
@ -32,6 +32,7 @@ Cu.import("resource://gre/modules/FxAccountsCommon.js");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
Cu.import("resource://services-common/hawkrequest.js");
|
||||
Cu.import("resource://services-common/observers.js");
|
||||
Cu.import("resource://gre/modules/Promise.jsm");
|
||||
|
||||
/*
|
||||
@ -75,6 +76,10 @@ this.HawkClient.prototype = {
|
||||
retryAfter = retryAfter ? parseInt(retryAfter) : retryAfter;
|
||||
if (retryAfter) {
|
||||
errorObj.retryAfter = retryAfter;
|
||||
// and notify observers of the retry interval
|
||||
if (this.observerPrefix) {
|
||||
Observers.notify(this.observerPrefix + ":backoff:interval", retryAfter);
|
||||
}
|
||||
}
|
||||
return errorObj;
|
||||
},
|
||||
@ -154,6 +159,11 @@ this.HawkClient.prototype = {
|
||||
" - Status text: " + restResponse.statusText,
|
||||
" - Response text: " + restResponse.body);
|
||||
|
||||
// All responses may have backoff headers, which are a server-side safety
|
||||
// valve to allow slowing down clients without hurting performance.
|
||||
self._maybeNotifyBackoff(restResponse, "x-weave-backoff");
|
||||
self._maybeNotifyBackoff(restResponse, "x-backoff");
|
||||
|
||||
if (error) {
|
||||
// When things really blow up, reconstruct an error object that follows
|
||||
// the general format of the server on error responses.
|
||||
@ -162,7 +172,7 @@ this.HawkClient.prototype = {
|
||||
|
||||
self._updateClockOffset(restResponse.headers["date"]);
|
||||
|
||||
if (status === 401 && retryOK) {
|
||||
if (status === 401 && retryOK && !("retry-after" in restResponse.headers)) {
|
||||
// Retry once if we were rejected due to a bad timestamp.
|
||||
// Clock offset is adjusted already in the top of this function.
|
||||
log.debug("Received 401 for " + path + ": retrying");
|
||||
@ -209,6 +219,35 @@ this.HawkClient.prototype = {
|
||||
return deferred.promise;
|
||||
},
|
||||
|
||||
/*
|
||||
* The prefix used for all notifications sent by this module. This
|
||||
* allows the handler of notifications to be sure they are handling
|
||||
* notifications for the service they expect.
|
||||
*
|
||||
* If not set, no notifications will be sent.
|
||||
*/
|
||||
observerPrefix: null,
|
||||
|
||||
// Given an optional header value, notify that a backoff has been requested.
|
||||
_maybeNotifyBackoff: function (response, headerName) {
|
||||
if (!this.observerPrefix) {
|
||||
return;
|
||||
}
|
||||
let headerVal = response.headers[headerName];
|
||||
if (!headerVal) {
|
||||
return;
|
||||
}
|
||||
let backoffInterval;
|
||||
try {
|
||||
backoffInterval = parseInt(headerVal, 10);
|
||||
} catch (ex) {
|
||||
this._log.error("hawkclient response had invalid backoff value in '" +
|
||||
headerName + "' header: " + headerVal);
|
||||
return;
|
||||
}
|
||||
Observers.notify(this.observerPrefix + ":backoff:interval", backoffInterval);
|
||||
},
|
||||
|
||||
// override points for testing.
|
||||
newHAWKAuthenticatedRESTRequest: function(uri, credentials, extra) {
|
||||
return new HAWKAuthenticatedRESTRequest(uri, credentials, extra);
|
||||
|
@ -329,7 +329,8 @@ TokenServerClient.prototype = {
|
||||
return;
|
||||
}
|
||||
|
||||
// Any response status can have an X-Backoff header.
|
||||
// Any response status can have X-Backoff or X-Weave-Backoff headers.
|
||||
this._maybeNotifyBackoff(response, "x-weave-backoff");
|
||||
this._maybeNotifyBackoff(response, "x-backoff");
|
||||
|
||||
// The service shouldn't have any 3xx, so we don't need to handle those.
|
||||
@ -413,8 +414,20 @@ TokenServerClient.prototype = {
|
||||
});
|
||||
},
|
||||
|
||||
/*
|
||||
* The prefix used for all notifications sent by this module. This
|
||||
* allows the handler of notifications to be sure they are handling
|
||||
* notifications for the service they expect.
|
||||
*
|
||||
* If not set, no notifications will be sent.
|
||||
*/
|
||||
observerPrefix: null,
|
||||
|
||||
// Given an optional header value, notify that a backoff has been requested.
|
||||
_maybeNotifyBackoff: function (response, headerName) {
|
||||
if (!this.observerPrefix) {
|
||||
return;
|
||||
}
|
||||
let headerVal = response.headers[headerName];
|
||||
if (!headerVal) {
|
||||
return;
|
||||
@ -427,7 +440,7 @@ TokenServerClient.prototype = {
|
||||
headerName + "' header: " + headerVal);
|
||||
return;
|
||||
}
|
||||
Observers.notify("tokenserver:backoff:interval", backoffInterval);
|
||||
Observers.notify(this.observerPrefix + ":backoff:interval", backoffInterval);
|
||||
},
|
||||
|
||||
// override points for testing.
|
||||
|
@ -460,9 +460,26 @@ FxAccountsInternal.prototype = {
|
||||
},
|
||||
|
||||
signOut: function signOut() {
|
||||
this.abortExistingFlow();
|
||||
this.currentAccountState.signedInUser = null; // clear in-memory cache
|
||||
return this.signedInUserStorage.set(null).then(() => {
|
||||
let currentState = this.currentAccountState;
|
||||
let fxAccountsClient = this.fxAccountsClient;
|
||||
let sessionToken;
|
||||
return currentState.getUserAccountData().then(data => {
|
||||
// Save the session token for use in the call to signOut below.
|
||||
sessionToken = data && data.sessionToken;
|
||||
this.abortExistingFlow();
|
||||
this.currentAccountState.signedInUser = null; // clear in-memory cache
|
||||
return this.signedInUserStorage.set(null);
|
||||
}).then(() => {
|
||||
// Wrap this in a promise so *any* errors in signOut won't
|
||||
// block the local sign out. This is *not* returned.
|
||||
Promise.resolve().then(() => {
|
||||
// This can happen in the background and shouldn't block
|
||||
// the user from signing out. The server must tolerate
|
||||
// clients just disappearing, so this call should be best effort.
|
||||
return fxAccountsClient.signOut(sessionToken);
|
||||
}).then(null, err => {
|
||||
log.error("Error during remote sign out of Firefox Accounts: " + err);
|
||||
});
|
||||
this.notifyObservers(ONLOGOUT_NOTIFICATION);
|
||||
});
|
||||
},
|
||||
@ -497,15 +514,18 @@ FxAccountsInternal.prototype = {
|
||||
}
|
||||
if (!currentState.whenKeysReadyDeferred) {
|
||||
currentState.whenKeysReadyDeferred = Promise.defer();
|
||||
this.fetchAndUnwrapKeys(data.keyFetchToken).then(data => {
|
||||
if (!data.kA || !data.kB) {
|
||||
currentState.whenKeysReadyDeferred.reject(
|
||||
new Error("user data missing kA or kB")
|
||||
);
|
||||
return;
|
||||
}
|
||||
currentState.whenKeysReadyDeferred.resolve(data);
|
||||
});
|
||||
this.fetchAndUnwrapKeys(data.keyFetchToken).then(
|
||||
data => {
|
||||
if (!data.kA || !data.kB) {
|
||||
currentState.whenKeysReadyDeferred.reject(
|
||||
new Error("user data missing kA or kB")
|
||||
);
|
||||
return;
|
||||
}
|
||||
currentState.whenKeysReadyDeferred.resolve(data);
|
||||
},
|
||||
err => currentState.whenKeysReadyDeferred.reject(err)
|
||||
);
|
||||
}
|
||||
return currentState.whenKeysReadyDeferred.promise;
|
||||
}).then(result => currentState.resolve(result));
|
||||
|
@ -23,6 +23,7 @@ this.FxAccountsClient = function(host = HOST) {
|
||||
// The FxA auth server expects requests to certain endpoints to be authorized
|
||||
// using Hawk.
|
||||
this.hawk = new HawkClient(host);
|
||||
this.hawk.observerPrefix = "FxA:hawk";
|
||||
|
||||
// Manage server backoff state. C.f.
|
||||
// https://github.com/mozilla/fxa-auth-server/blob/master/docs/api.md#backoff-protocol
|
||||
|
@ -77,6 +77,8 @@ function MockFxAccountsClient() {
|
||||
|
||||
this.signCertificate = function() { throw "no" };
|
||||
|
||||
this.signOut = function() { return Promise.resolve(); };
|
||||
|
||||
FxAccountsClient.apply(this);
|
||||
}
|
||||
MockFxAccountsClient.prototype = {
|
||||
@ -537,6 +539,45 @@ add_test(function test_resend_email() {
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_sign_out() {
|
||||
do_test_pending();
|
||||
let fxa = new MockFxAccounts();
|
||||
let remoteSignOutCalled = false;
|
||||
let client = fxa.internal.fxAccountsClient;
|
||||
client.signOut = function() { remoteSignOutCalled = true; return Promise.resolve(); };
|
||||
makeObserver(ONLOGOUT_NOTIFICATION, function() {
|
||||
log.debug("test_sign_out_with_remote_error observed onlogout");
|
||||
// user should be undefined after sign out
|
||||
fxa.internal.getUserAccountData().then(user => {
|
||||
do_check_eq(user, null);
|
||||
do_check_true(remoteSignOutCalled);
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
});
|
||||
});
|
||||
fxa.signOut();
|
||||
});
|
||||
|
||||
add_test(function test_sign_out_with_remote_error() {
|
||||
do_test_pending();
|
||||
let fxa = new MockFxAccounts();
|
||||
let client = fxa.internal.fxAccountsClient;
|
||||
let remoteSignOutCalled = false;
|
||||
// Force remote sign out to trigger an error
|
||||
client.signOut = function() { remoteSignOutCalled = true; throw "Remote sign out error"; };
|
||||
makeObserver(ONLOGOUT_NOTIFICATION, function() {
|
||||
log.debug("test_sign_out_with_remote_error observed onlogout");
|
||||
// user should be undefined after sign out
|
||||
fxa.internal.getUserAccountData().then(user => {
|
||||
do_check_eq(user, null);
|
||||
do_check_true(remoteSignOutCalled);
|
||||
do_test_finished();
|
||||
run_next_test();
|
||||
});
|
||||
});
|
||||
fxa.signOut();
|
||||
});
|
||||
|
||||
/*
|
||||
* End of tests.
|
||||
* Utility functions follow.
|
||||
|
@ -387,6 +387,21 @@ thisPingDate
|
||||
version
|
||||
Integer version of this payload format. Currently only 1 is defined.
|
||||
|
||||
clientID
|
||||
An identifier that identifies the client that is submitting data.
|
||||
|
||||
This property may not be present in older clients.
|
||||
|
||||
See :ref:`healthreport_identifiers` for more info on identifiers.
|
||||
|
||||
clientIDVersion
|
||||
Integer version associated with the generation semantics for the
|
||||
``clientID``.
|
||||
|
||||
If the value is ``1``, ``clientID`` is a randomly-generated UUID.
|
||||
|
||||
This property may not be present in older clients.
|
||||
|
||||
data
|
||||
Object holding data constituting health report.
|
||||
|
||||
|
83
services/healthreport/docs/identifiers.rst
Normal file
@ -0,0 +1,83 @@
|
||||
.. _healthreport_identifiers:
|
||||
|
||||
===========
|
||||
Identifiers
|
||||
===========
|
||||
|
||||
Firefox Health Report records some identifiers to keep track of clients
|
||||
and uploaded documents.
|
||||
|
||||
Identifier Types
|
||||
================
|
||||
|
||||
Document/Upload IDs
|
||||
-------------------
|
||||
|
||||
A random UUID called the *Document ID* or *Upload ID* is generated when the FHR
|
||||
client creates or uploads a new document.
|
||||
|
||||
When clients generate a new *Document ID*, they persist this ID to disk
|
||||
**before** the upload attempt.
|
||||
|
||||
As part of the upload, the client sends all old *Document IDs* to the server
|
||||
and asks the server to delete them. In well-behaving clients, the server
|
||||
has a single record for each client with a randomly-changing *Document ID*.
|
||||
|
||||
Client IDs
|
||||
----------
|
||||
|
||||
A *Client ID* is an identifier that **attempts** to uniquely identify an
|
||||
individual FHR client. Please note the emphasis on *attempts* in that last
|
||||
sentence: *Client IDs* do not guarantee uniqueness.
|
||||
|
||||
The *Client ID* is generated when the client first runs or as needed.
|
||||
|
||||
The *Client ID* is transferred to the server as part of every upload. The
|
||||
server is thus able to affiliate multiple document uploads with a single
|
||||
*Client ID*.
|
||||
|
||||
Client ID Versions
|
||||
^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The semantics for how a *Client ID* is generated are versioned.
|
||||
|
||||
Version 1
|
||||
The *Client ID* is a randomly-generated UUID.
|
||||
|
||||
History of Identifiers
|
||||
======================
|
||||
|
||||
In the beginning, there were just *Document IDs*. The thinking was clients
|
||||
would clean up after themselves and leave at most 1 active document on the
|
||||
server.
|
||||
|
||||
Unfortunately, this did not work out. Using brute force analysis to
|
||||
deduplicate records on the server, a number of interesting patterns emerged.
|
||||
|
||||
Orphaning
|
||||
Clients would upload a new payload while not deleting the old payload.
|
||||
|
||||
Divergent records
|
||||
Records would share data up to a certain date and then the data would
|
||||
almost completely diverge. This appears to be indicative of profile
|
||||
copying.
|
||||
|
||||
Rollback
|
||||
Records would share data up to a certain date. Each record in this set
|
||||
would contain data for a day or two but no extra data. This could be
|
||||
explained by filesystem rollback on the client.
|
||||
|
||||
A significant percentage of the records on the server belonged to
|
||||
misbehaving clients. Identifying these records was extremely resource
|
||||
intensive and error-prone. These records were undermining the ability
|
||||
to use Firefox Health Report data.
|
||||
|
||||
Thus, the *Client ID* was born. The intent of the *Client ID* was to
|
||||
uniquely identify clients so the extreme effort required and the
|
||||
questionable reliability of deduplicating server data would become
|
||||
problems of the past.
|
||||
|
||||
The *Client ID* was originally a randomly-generated UUID (version 1). This
|
||||
allowed detection of orphaning and rollback. However, these version 1
|
||||
*Client IDs* were still susceptible to use on multiple profiles and
|
||||
machines if the profile was copied.
|
@ -24,6 +24,7 @@ are very specific to what the Firefox Health Report feature requires.
|
||||
|
||||
architecture
|
||||
dataformat
|
||||
identifiers
|
||||
|
||||
Legal and Privacy Concerns
|
||||
==========================
|
||||
|