Merge fx-team to m-c.

This commit is contained in:
Ryan VanderMeulen 2014-04-01 16:31:56 -04:00
commit a8d050a8aa
92 changed files with 1088 additions and 398 deletions

View File

@ -116,17 +116,13 @@ exports["test nested frames"] = function(assert, done) {
});
};
// ignore about:blank pages and *-document-global-created
// events that are not very consistent.
// ignore http:// requests, as Fennec's `about:home` page
// displays add-ons a user could install
// ignore local `searchplugins` files loaded
// ignore *-document-global-created events that are not very consistent.
// only allow data uris that we create to ignore unwanted events, e.g.,
// about:blank, http:// requests from Fennec's `about:`home page that displays
// add-ons a user could install, local `searchplugins`, other chrome uris
// Calls callback if passes filter
function eventFilter (type, target, callback) {
if (target.URL !== "about:blank" &&
target.URL !== "about:home" &&
!target.URL.match(/^https?:\/\//i) &&
!target.URL.match(/searchplugins/) &&
if (target.URL.startsWith("data:text/html,") &&
type !== "chrome-document-global-created" &&
type !== "content-document-global-created")

View File

@ -1208,9 +1208,6 @@ pref("devtools.shadereditor.enabled", false);
// Enable the Canvas Debugger.
pref("devtools.canvasdebugger.enabled", false);
// Enable tools for Chrome development.
pref("devtools.chrome.enabled", false);
// Default theme ("dark" or "light")
pref("devtools.theme", "light");

View File

@ -3995,10 +3995,12 @@ var TabsProgressListener = {
// Collect telemetry data about tab load times.
if (aWebProgress.isTopLevel) {
if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
if (aStateFlags & Ci.nsIWebProgressListener.STATE_START)
if (aStateFlags & Ci.nsIWebProgressListener.STATE_START) {
TelemetryStopwatch.start("FX_PAGE_LOAD_MS", aBrowser);
else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP)
Services.telemetry.getHistogramById("FX_TOTAL_TOP_VISITS").add(true);
} else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) {
TelemetryStopwatch.finish("FX_PAGE_LOAD_MS", aBrowser);
}
} else if (aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
aStatus == Cr.NS_BINDING_ABORTED) {
TelemetryStopwatch.cancel("FX_PAGE_LOAD_MS", aBrowser);

View File

@ -207,3 +207,16 @@ input[type=button] {
background-color: #fff;
opacity: 0.01;
}
/* PANEL */
#sponsored-panel {
width: 330px;
}
#sponsored-panel description {
margin: 0;
}
#sponsored-panel .text-link {
margin: 12px 0 0;
}

View File

@ -17,6 +17,14 @@
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
title="&newtab.pageTitle;">
<xul:panel id="sponsored-panel" orient="vertical" type="arrow">
<xul:description>&newtab.panel.message;</xul:description>
<xul:label class="text-link"
href="&newtab.panel.link.url;"
onclick="this.parentNode.hidePopup();"
value="&newtab.panel.link.text;" />
</xul:panel>
<div id="newtab-scrollbox">
<div id="newtab-vertical-margin">

View File

@ -23,6 +23,9 @@ let gPage = {
let button = document.getElementById("newtab-toggle");
button.addEventListener("click", this, false);
// Initialize sponsored panel
this._sponsoredPanel = document.getElementById("sponsored-panel");
// Check if the new tab feature is enabled.
let enabled = gAllPages.enabled;
if (enabled)
@ -80,6 +83,21 @@ let gPage = {
}
},
/**
* Shows sponsored panel
*/
showSponsoredPanel: function Page_showSponsoredPanel(aTarget) {
if (this._sponsoredPanel.state == "closed") {
let self = this;
this._sponsoredPanel.addEventListener("popuphidden", function onPopupHidden(aEvent) {
self._sponsoredPanel.removeEventListener("popuphidden", onPopupHidden, false);
aTarget.removeAttribute("panelShown");
});
}
aTarget.setAttribute("panelShown", "true");
this._sponsoredPanel.openPopup(aTarget);
},
/**
* Internally initializes the page. This runs only when/if the feature
* is/gets enabled.

View File

@ -224,6 +224,8 @@ Site.prototype = {
aEvent.preventDefault();
if (aEvent.target.classList.contains("newtab-control-block"))
this.block();
else if (target.classList.contains("newtab-control-sponsored"))
gPage.showSponsoredPanel(target);
else if (this.isPinned())
this.unpin();
else

View File

@ -1668,7 +1668,7 @@ nsContextMenu.prototype = {
// Formats the 'Search <engine> for "<selection or link text>"' context menu.
formatSearchContextItem: function() {
var menuItem = document.getElementById("context-searchselect");
var selectedText = this.onLink ? this.linkText() : this.textSelected;
var selectedText = this.isTextSelected ? this.textSelected : this.linkText();
// Store searchTerms in context menu item so we know what to search onclick
menuItem.searchTerms = selectedText;

View File

@ -229,6 +229,7 @@ skip-if = true # browser_drag.js is disabled, as it needs to be updated for the
[browser_keywordSearch.js]
[browser_keywordSearch_postData.js]
[browser_lastAccessedTab.js]
skip-if = toolkit == "windows" # Disabled on Windows due to frequent failures (bug 969405)
[browser_locationBarCommand.js]
skip-if = os == "linux" # Intermittent failures, bug 917535
[browser_locationBarExternalLoad.js]

View File

@ -94,6 +94,20 @@ function test() {
shouldBeShown: false,
});
testElement({
id: "partialLink",
isSelected: true,
shouldBeShown: true,
expectedLabelContents: "link selection",
});
testElement({
id: "partialLink",
isSelected: false,
shouldBeShown: true,
expectedLabelContents: "A partial link " + ellipsis,
});
// cleanup
document.popupNode = null;
gBrowser.removeCurrentTab();

View File

@ -10,5 +10,7 @@
<span id="mixedContent">
I'm some text, and <a href="http://mozilla.org">I'm a link!</a>
</span>
<a href="http://mozilla.org">A partial <span id="partialLink">link selection</span></a>
</body>
</html>

View File

@ -21,6 +21,7 @@ skip-if = os == "mac" # Intermittent failures, bug 898317
[browser_newtab_focus.js]
[browser_newtab_perwindow_private_browsing.js]
[browser_newtab_reset.js]
[browser_newtab_sponsored_icon_click.js]
[browser_newtab_tabsync.js]
[browser_newtab_undo.js]
[browser_newtab_unpin.js]

View File

@ -0,0 +1,33 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
function runTests() {
yield setLinks("0");
yield addNewTabPageTab();
let site = getCell(0).node.querySelector(".newtab-site");
site.setAttribute("type", "sponsored");
let sponsoredPanel = getContentDocument().getElementById("sponsored-panel");
is(sponsoredPanel.state, "closed", "Sponsored panel must be closed");
function continueOnceOn(event) {
sponsoredPanel.addEventListener(event, function listener() {
sponsoredPanel.removeEventListener(event, listener);
executeSoon(TestRunner.next);
});
}
// test sponsoredPanel appearing upon a click
continueOnceOn("popupshown");
let sponsoredButton = site.querySelector(".newtab-control-sponsored");
yield EventUtils.synthesizeMouseAtCenter(sponsoredButton, {}, getContentWindow());
is(sponsoredPanel.state, "open", "Sponsored panel opens on click");
ok(sponsoredButton.hasAttribute("panelShown"), "Sponsored button has panelShown attribute");
// test sponsoredPanel hiding
continueOnceOn("popuphidden");
yield sponsoredPanel.hidePopup();
is(sponsoredPanel.state, "closed", "Sponsored panel correctly closed/hidden");
ok(!sponsoredButton.hasAttribute("panelShown"), "Sponsored button does not have panelShown attribute");
}

View File

@ -31,10 +31,10 @@
<label value="&options.selectDevToolsTheme.label;"/>
<radiogroup id="devtools-theme-box"
class="options-groupbox"
data-pref="devtools.theme"
orient="horizontal">
<radio checked="true" value="light" label="Metal"/>
<radio checked="false" disabled="true" value="light" label="&options.lightTheme.label;"/>
<radio checked="false" disabled="true" value="dark" label="&options.darkTheme.label;"/>
<radio value="light" label="&options.lightTheme.label;"/>
<radio value="dark" label="&options.darkTheme.label;"/>
</radiogroup>
<label value="&options.commonPrefs.label;"/>
<vbox id="commonprefs-options" class="options-groupbox">

View File

@ -114,6 +114,11 @@
<key id="key_openHelp"
keycode="VK_F1"
command="sp-cmd-documentationLink"/>
<key id="key_gotoLine"
key="&gotoLineCmd.key;"
command="key_gotoLine"
modifiers="accel"/>
</keyset>
<menubar id="sp-menubar">

View File

@ -79,7 +79,7 @@
const StylesheetUtils = devtools.require("sdk/stylesheet/utils");
let theme = Services.prefs.getCharPref("devtools.theme");
switchTheme("light");
switchTheme(theme);
gDevTools.on("pref-changed", handlePrefChange);
window.addEventListener("unload", function() {

View File

@ -15,12 +15,13 @@ Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/PluralForm.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/osfile.jsm");
let promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js").Promise;
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/devtools/event-emitter.js");
Cu.import("resource:///modules/devtools/gDevTools.jsm");
Cu.import("resource:///modules/devtools/StyleEditorUtil.jsm");
Cu.import("resource:///modules/devtools/SplitView.jsm");
Cu.import("resource:///modules/devtools/StyleSheetEditor.jsm");
const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
const { PrefObserver, PREF_ORIG_SOURCES } = require("devtools/styleeditor/utils");
@ -429,60 +430,69 @@ StyleEditorUI.prototype = {
}
}, false);
Task.spawn(function* () {
// autofocus if it's a new user-created stylesheet
if (editor.isNew) {
this._selectEditor(editor);
yield this._selectEditor(editor);
}
if (this._styleSheetToSelect
&& this._styleSheetToSelect.href == editor.styleSheet.href) {
this.switchToSelectedSheet();
yield this.switchToSelectedSheet();
}
// If this is the first stylesheet and there is no pending request to
// select a particular style sheet, select this sheet.
if (!this.selectedEditor && !this._styleSheetBoundToSelect
&& editor.styleSheet.styleSheetIndex == 0) {
this._selectEditor(editor);
yield this._selectEditor(editor);
}
this.emit("editor-added", editor);
}.bind(this)).then(null, Cu.reportError);
}.bind(this),
onShow: function(summary, details, data) {
let editor = data.editor;
this.selectedEditor = editor;
Task.spawn(function* () {
if (!editor.sourceEditor) {
// only initialize source editor when we switch to this view
let inputElement = details.querySelector(".stylesheet-editor-input");
editor.load(inputElement);
yield editor.load(inputElement);
}
editor.onShow();
this.emit("editor-selected", editor);
}.bind(this)).then(null, Cu.reportError);
}.bind(this)
});
},
/**
* Switch to the editor that has been marked to be selected.
*
* @return {Promise}
* Promise that will resolve when the editor is selected.
*/
switchToSelectedSheet: function() {
let sheet = this._styleSheetToSelect;
for each (let editor in this.editors) {
for (let editor of this.editors) {
if (editor.styleSheet.href == sheet.href) {
// The _styleSheetBoundToSelect will always hold the latest pending
// requested style sheet (with line and column) which is not yet
// selected by the source editor. Only after we select that particular
// editor and go the required line and column, it will become null.
this._styleSheetBoundToSelect = this._styleSheetToSelect;
this._selectEditor(editor, sheet.line, sheet.col);
this._styleSheetToSelect = null;
return;
return this._selectEditor(editor, sheet.line, sheet.col);
}
}
return promise.resolve();
},
/**
@ -494,19 +504,23 @@ StyleEditorUI.prototype = {
* Line number to jump to
* @param {number} col
* Column number to jump to
* @return {Promise}
* Promise that will resolve when the editor is selected.
*/
_selectEditor: function(editor, line, col) {
line = line || 0;
col = col || 0;
editor.getSourceEditor().then(() => {
let editorPromise = editor.getSourceEditor().then(() => {
editor.sourceEditor.setCursor({line: line, ch: col});
this._styleSheetBoundToSelect = null;
});
this.getEditorSummary(editor).then((summary) => {
let summaryPromise = this.getEditorSummary(editor).then((summary) => {
this._view.activeSummary = summary;
})
});
return promise.all([editorPromise, summaryPromise]);
},
getEditorSummary: function(editor) {

View File

@ -13,7 +13,7 @@ const Cu = Components.utils;
const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
const Editor = require("devtools/sourceeditor/editor");
const promise = require("sdk/core/promise");
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
const {CssLogic} = require("devtools/styleinspector/css-logic");
const AutoCompleter = require("devtools/sourceeditor/autocomplete");
@ -246,6 +246,9 @@ StyleSheetEditor.prototype = {
* Create source editor and load state into it.
* @param {DOMElement} inputElement
* Element to load source editor in
*
* @return {Promise}
* Promise that will resolve when the style editor is loaded.
*/
load: function(inputElement) {
this._inputElement = inputElement;
@ -261,7 +264,9 @@ StyleSheetEditor.prototype = {
};
let sourceEditor = new Editor(config);
sourceEditor.appendTo(inputElement).then(() => {
sourceEditor.on("dirty-change", this._onPropertyChange);
return sourceEditor.appendTo(inputElement).then(() => {
if (Services.prefs.getBoolPref(AUTOCOMPLETION_PREF)) {
sourceEditor.extend(AutoCompleter);
sourceEditor.setupAutoCompletion(this.walker);
@ -289,8 +294,6 @@ StyleSheetEditor.prototype = {
this.emit("source-editor-load");
});
sourceEditor.on("dirty-change", this._onPropertyChange);
},
/**

View File

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

View File

@ -88,16 +88,14 @@ function test()
{
waitForExplicitFinish();
addTabAndOpenStyleEditor(function(panel) {
panel.UI.on("editor-added", testEditorAdded);
});
addTabAndOpenStyleEditors(1, testEditorAdded);
content.location = TESTCASE_URI;
}
function testEditorAdded(aEvent, aEditor) {
function testEditorAdded(panel) {
info("Editor added, getting the source editor and starting tests");
aEditor.getSourceEditor().then(editor => {
panel.UI.editors[0].getSourceEditor().then(editor => {
info("source editor found, starting tests.");
gEditor = editor.sourceEditor;
gPopup = gEditor.getAutocompletionPopup();
@ -184,16 +182,14 @@ function testAutocompletionDisabled() {
info("Starting test to check if autocompletion is disabled correctly.")
Services.prefs.setBoolPref(AUTOCOMPLETION_PREF, false);
addTabAndOpenStyleEditor(function(panel) {
panel.UI.on("editor-added", testEditorAddedDisabled);
});
addTabAndOpenStyleEditors(1, testEditorAddedDisabled);
content.location = TESTCASE_URI;
}
function testEditorAddedDisabled(aEvent, aEditor) {
function testEditorAddedDisabled(panel) {
info("Editor added, getting the source editor and starting tests");
aEditor.getSourceEditor().then(editor => {
panel.UI.editors[0].getSourceEditor().then(editor => {
ok(!editor.sourceEditor.getAutocompletionPopup,
"Autocompletion popup does not exist");
cleanup();

View File

@ -70,21 +70,9 @@ function test()
const EXPECTED_STYLE_SHEET_COUNT = 12;
waitForExplicitFinish();
let styleSheetCount = 0;
addTabAndOpenStyleEditor(function (aPanel) {
aPanel.UI.on("editor-added", function () {
++styleSheetCount;
info(styleSheetCount+" out of "+
EXPECTED_STYLE_SHEET_COUNT+" style sheets loaded");
if (styleSheetCount == EXPECTED_STYLE_SHEET_COUNT) {
ok(true, "all style sheets loaded");
// The right number of events have been received; check that
// they actually show up in the style editor UI.
is(aPanel.UI.editors.length, EXPECTED_STYLE_SHEET_COUNT,
"UI elements present");
finish();
}
});
});
// Wait for events until the right number of editors has been opened.
addTabAndOpenStyleEditors(EXPECTED_STYLE_SHEET_COUNT, () => finish());
content.location = TESTCASE_URI;
}

View File

@ -8,24 +8,16 @@ let gUI;
function test() {
waitForExplicitFinish();
let count = 0;
addTabAndOpenStyleEditor(function(panel) {
gUI = panel.UI;
gUI.on("editor-added", function(event, editor) {
count++;
if (count == 4) {
info("all editors added");
runTests();
}
})
});
addTabAndOpenStyleEditors(4, runTests);
content.location = TESTCASE_URI;
}
let timeoutID;
function runTests() {
function runTests(panel) {
gUI = panel.UI;
gBrowser.tabContainer.addEventListener("TabOpen", onTabAdded, false);
gUI.editors[0].getSourceEditor().then(onEditor0Attach);
gUI.editors[1].getSourceEditor().then(onEditor1Attach);
@ -50,6 +42,8 @@ function onEditor0Attach(aEditor) {
function onEditor1Attach(aEditor) {
info("second editor selected");
// Wait for the focus to be set.
executeSoon(function () {
ok(aEditor.sourceEditor.hasFocus(),
"left mouse click has given editor 1 focus");
@ -60,6 +54,7 @@ function onEditor1Attach(aEditor) {
gPanelWindow);
setTimeout(finish, 0);
});
}
function onTabAdded() {

View File

@ -18,7 +18,7 @@ function test()
].join("\n"));
waitForExplicitFinish();
addTabAndOpenStyleEditor(function (aPanel) {
addTabAndOpenStyleEditors(1, function (aPanel) {
let UI = aPanel.UI;
// Spam the _onNewDocument callback multiple times before the

View File

@ -10,16 +10,11 @@ function test()
waitForExplicitFinish();
let count = 0;
addTabAndOpenStyleEditor(function(panel) {
let UI = panel.UI;
UI.on("editor-added", function(event, editor) {
count++;
if (count == 2) {
addTabAndOpenStyleEditors(2, function(panel) {
// we test against first stylesheet after all are ready
let UI = panel.UI;
let editor = UI.editors[0];
editor.getSourceEditor().then(runTests.bind(this, UI, editor));
}
})
});
content.location = TESTCASE_URI;

View File

@ -20,15 +20,10 @@ function test()
copy(TESTCASE_URI_HTML, "simple.html", function(htmlFile) {
copy(TESTCASE_URI_CSS, "simple.css", function(cssFile) {
addTabAndOpenStyleEditor(function(panel) {
addTabAndOpenStyleEditors(1, function(panel) {
let UI = panel.UI;
UI.on("editor-added", function(event, editor) {
if (editor.styleSheet.styleSheetIndex != 0) {
return; // we want to test against the first stylesheet
}
let editor = UI.editors[0];
editor.getSourceEditor().then(runTests.bind(this, editor));
})
});
let uri = Services.io.newFileURI(htmlFile);

View File

@ -19,10 +19,7 @@ function test()
{
waitForExplicitFinish();
addTabAndOpenStyleEditor(function(panel) {
gUI = panel.UI;
gUI.on("editor-added", testEditorAdded);
});
addTabAndCheckOnStyleEditorAdded(panel => gUI = panel.UI, testEditorAdded);
content.location = TESTCASE_URI;
}
@ -53,7 +50,7 @@ function testImport()
}
let gAddedCount = 0;
function testEditorAdded(aEvent, aEditor)
function testEditorAdded(aEditor)
{
if (++gAddedCount == 2) {
// test import after the 2 initial stylesheets have been loaded

View File

@ -11,20 +11,14 @@ function test()
{
waitForExplicitFinish();
addTabAndOpenStyleEditor(function(panel) {
gUI = panel.UI;
gUI.on("editor-added", onEditorAdded);
});
addTabAndOpenStyleEditors(3, onEditorAdded);
content.location = TESTCASE_URI;
}
let gAddedCount = 0;
function onEditorAdded()
function onEditorAdded(panel)
{
if (++gAddedCount != 3) {
return;
}
gUI = panel.UI;
is(gUI.editors.length, 3,
"there are 3 stylesheets after loading @imports");

View File

@ -10,16 +10,13 @@ function test()
{
waitForExplicitFinish();
addTabAndOpenStyleEditor(function(panel) {
gUI = panel.UI;
gUI.on("editor-added", testEditorAdded);
});
addTabAndCheckOnStyleEditorAdded(panel => gUI = panel.UI, testEditorAdded);
content.location = TESTCASE_URI;
}
let gEditorAddedCount = 0;
function testEditorAdded(aEvent, aEditor)
function testEditorAdded(aEditor)
{
if (aEditor.styleSheet.styleSheetIndex == 0) {
gEditorAddedCount++;

View File

@ -12,15 +12,14 @@ function test()
{
waitForExplicitFinish();
addTabAndOpenStyleEditor(function(panel) {
addTabAndOpenStyleEditors(2, function(panel) {
gUI = panel.UI;
// First test that identifiers are correcly generated. If not other tests
// are likely to fail.
testIndentifierGeneration();
waitForEditors(2)
.then(saveFirstInlineStyleSheet)
saveFirstInlineStyleSheet()
.then(testFriendlyNamesAfterSave)
.then(reloadPage)
.then(testFriendlyNamesAfterSave)
@ -93,10 +92,9 @@ function navigateToAnotherPage() {
gUI = null;
addTabAndOpenStyleEditor(function(panel) {
addTabAndOpenStyleEditors(2, function(panel) {
gUI = panel.UI;
waitForEditors(2).then(deferred.resolve);
deferred.resolve();
});
content.location = SECOND_TEST_PAGE;

View File

@ -14,11 +14,9 @@ function test()
// *while* the page is still loading. The Style Editor should not signal that
// it is loaded until the accompanying content page is loaded.
addTabAndOpenStyleEditor(function(panel) {
panel.UI.on("editor-added", testEditorAdded);
addTabAndCheckOnStyleEditorAdded(function(panel) {
content.location = TESTCASE_URI;
});
}, testEditorAdded);
}
function testEditorAdded(event, editor)

View File

@ -13,10 +13,7 @@ function test()
{
waitForExplicitFinish();
addTabAndOpenStyleEditor(function(panel) {
gUI = panel.UI;
gUI.on("editor-added", testEditorAdded);
});
addTabAndCheckOnStyleEditorAdded(panel => gUI = panel.UI, testEditorAdded);
content.location = TESTCASE_URI;
}
@ -27,10 +24,10 @@ let gOriginalHref;
let checksCompleted = 0;
function testEditorAdded(aEvent, aEditor)
function testEditorAdded(aEditor)
{
gAddedCount++;
if (gAddedCount == 2) {
info("added " + gAddedCount + " editors");
if (++gAddedCount == 2) {
waitForFocus(function () {// create a new style sheet
let newButton = gPanelWindow.document.querySelector(".style-editor-newButton");
ok(newButton, "'new' button exists");

View File

@ -14,11 +14,11 @@ function test()
// *while* the page is still loading. The Style Editor should not signal that
// it is loaded until the accompanying content page is loaded.
addTabAndOpenStyleEditor(function(panel) {
addTabAndCheckOnStyleEditorAdded(function(panel) {
panel.UI.once("stylesheets-reset", testDocumentLoad);
content.location = TESTCASE_URI;
});
}, () => {});
}
function testDocumentLoad(event)

View File

@ -10,14 +10,11 @@ function test()
{
waitForExplicitFinish();
addTabAndOpenStyleEditor(function(panel) {
gUI = panel.UI;
gUI.on("editor-added", function(event, editor) {
addTabAndCheckOnStyleEditorAdded(panel => gUI = panel.UI, editor => {
editor.getSourceEditor().then(function() {
testEditor(editor);
});
});
});
content.location = TESTCASE_URI;
}

View File

@ -15,18 +15,10 @@ function test()
{
waitForExplicitFinish();
addTabAndOpenStyleEditor(function(panel) {
addTabAndOpenStyleEditors(2, function(panel) {
gContentWin = gBrowser.selectedTab.linkedBrowser.contentWindow.wrappedJSObject;
gUI = panel.UI;
let count = 0;
gUI.on("editor-added", function editorAdded(event, editor) {
if (++count == 2) {
info("all editors added to UI");
gUI.off("editor-added", editorAdded);
gUI.editors[0].getSourceEditor().then(runTests);
}
})
});
content.location = TESTCASE_URI;

View File

@ -15,17 +15,10 @@ function test()
{
waitForExplicitFinish();
addTabAndOpenStyleEditor(function(panel) {
addTabAndOpenStyleEditors(2, function(panel) {
gContentWin = gBrowser.selectedTab.linkedBrowser.contentWindow.wrappedJSObject;
gUI = panel.UI;
let count = 0;
gUI.on("editor-added", function editorAdded(event, editor) {
if (++count == 2) {
gUI.off("editor-added", editorAdded);
gUI.editors[0].getSourceEditor().then(runTests);
}
})
});
content.location = TESTCASE_URI;

View File

@ -71,14 +71,9 @@ function test()
function openEditor(testcaseURI) {
let deferred = promise.defer();
addTabAndOpenStyleEditor((panel) => {
info("style editor panel opened");
addTabAndOpenStyleEditors(3, panel => {
let UI = panel.UI;
let count = 0;
UI.on("editor-added", (event, editor) => {
if (++count == 3) {
// wait for 3 editors - 1 for first style sheet, 1 for the
// generated style sheet, and 1 for original source after it
// loads and replaces the generated style sheet.
@ -88,9 +83,7 @@ function openEditor(testcaseURI) {
link.click();
editor.getSourceEditor().then(deferred.resolve);
}
});
})
content.location = testcaseURI;
return deferred.promise;

View File

@ -12,18 +12,10 @@ function test()
Services.prefs.setBoolPref(PREF, true);
let count = 0;
addTabAndOpenStyleEditor(function(panel) {
let UI = panel.UI;
UI.on("editor-added", (event, editor) => {
if (++count == 3) {
// wait for 3 editors - 1 for first style sheet, 1 for the
// generated style sheet, and 1 for original source after it
// loads and replaces the generated style sheet.
runTests(UI);
}
})
});
addTabAndOpenStyleEditors(3, panel => runTests(panel.UI));
content.location = TESTCASE_URI;
}

View File

@ -10,20 +10,14 @@ function test()
{
waitForExplicitFinish();
addTabAndOpenStyleEditor(function(panel) {
gUI = panel.UI;
gUI.on("editor-added", function(event, editor) {
if (editor == gUI.editors[3]) {
runTests();
}
});
});
addTabAndOpenStyleEditors(4, runTests);
content.location = TESTCASE_URI;
}
function runTests()
function runTests(panel)
{
gUI = panel.UI;
gUI.editors[0].getSourceEditor().then(onEditor0Attach);
gUI.editors[2].getSourceEditor().then(onEditor2Attach);
}
@ -70,9 +64,12 @@ function onEditor0Attach(aEditor)
function onEditor2Attach(aEditor)
{
// Wait for the focus to be set.
executeSoon(function () {
ok(aEditor.sourceEditor.hasFocus(),
"editor 2 has focus");
gUI = null;
finish();
});
}

View File

@ -11,15 +11,7 @@ function test()
{
waitForExplicitFinish();
addTabAndOpenStyleEditor(function(panel) {
let UI = panel.UI;
UI.on("editor-added", function(event, editor) {
if (editor == UI.editors[1]) {
// wait until both editors are added
runTests(UI);
}
});
});
addTabAndOpenStyleEditors(2, panel => runTests(panel.UI));
content.location = TESTCASE_URI;
}

View File

@ -36,11 +36,32 @@ function cleanup()
}
}
function addTabAndOpenStyleEditor(callback) {
function addTabAndOpenStyleEditors(count, callback) {
let currentCount = 0;
let panel;
addTabAndCheckOnStyleEditorAdded(p => panel = p, function () {
currentCount++;
info(currentCount + " of " + count + " editors opened");
if (currentCount == count) {
callback(panel);
}
});
}
function addTabAndCheckOnStyleEditorAdded(callbackOnce, callbackOnAdded) {
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", function onLoad() {
gBrowser.selectedBrowser.removeEventListener("load", onLoad, true);
openStyleEditorInWindow(window, callback);
openStyleEditorInWindow(window, function (panel) {
// Execute the individual callback with the panel argument.
callbackOnce(panel);
// Report editors that already opened while loading.
for (let editor of panel.UI.editors) {
callbackOnAdded(editor);
}
// Report new editors added afterwards.
panel.UI.on("editor-added", (event, editor) => callbackOnAdded(editor));
});
}, true);
}

View File

@ -9,7 +9,7 @@ const {Cc, Ci, Cu} = require("chrome");
const ToolDefinitions = require("main").Tools;
const {CssLogic} = require("devtools/styleinspector/css-logic");
const {ELEMENT_STYLE} = require("devtools/server/actors/styles");
const promise = require("sdk/core/promise");
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
const {EventEmitter} = require("devtools/toolkit/event-emitter");
const {OutputParser} = require("devtools/output-parser");
const {Tooltip} = require("devtools/shared/widgets/Tooltip");

View File

@ -7,7 +7,7 @@
"use strict";
const {Cc, Ci, Cu} = require("chrome");
const promise = require("sdk/core/promise");
const {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
const {CssLogic} = require("devtools/styleinspector/css-logic");
const {InplaceEditor, editableField, editableItem} = require("devtools/shared/inplace-editor");
const {ELEMENT_STYLE, PSEUDO_ELEMENTS} = require("devtools/server/actors/styles");
@ -129,16 +129,6 @@ function ElementStyle(aElement, aStore, aPageStyle) {
if (!("disabled" in this.store)) {
this.store.disabled = new WeakMap();
}
// To figure out how shorthand properties are interpreted by the
// engine, we will set properties on a dummy element and observe
// how their .style attribute reflects them as computed values.
this.dummyElementPromise = createDummyDocument().then(document => {
this.dummyElement = document.createElementNS(this.element.namespaceURI,
this.element.tagName);
document.documentElement.appendChild(this.dummyElement);
return this.dummyElement;
}).then(null, promiseWarn);
}
// We're exporting _ElementStyle for unit tests.
@ -152,6 +142,19 @@ ElementStyle.prototype = {
// to figure out how shorthand properties will be parsed.
dummyElement: null,
init: function()
{
// To figure out how shorthand properties are interpreted by the
// engine, we will set properties on a dummy element and observe
// how their .style attribute reflects them as computed values.
return this.dummyElementPromise = createDummyDocument().then(document => {
this.dummyElement = document.createElementNS(this.element.namespaceURI,
this.element.tagName);
document.documentElement.appendChild(this.dummyElement);
return this.dummyElement;
}).then(null, promiseWarn);
},
destroy: function() {
this.dummyElement = null;
this.dummyElementPromise.then(dummyElement => {
@ -1383,7 +1386,9 @@ CssRuleView.prototype = {
}
this._elementStyle = new ElementStyle(aElement, this.store, this.pageStyle);
return this._populate().then(() => {
return this._elementStyle.init().then(() => {
return this._populate();
}).then(() => {
// A new node may already be selected, in which this._elementStyle will
// be null.
if (this._elementStyle) {

View File

@ -23,7 +23,7 @@ let {CssHtmlTree} = devtools.require("devtools/styleinspector/computed-view");
let {CssRuleView, _ElementStyle} = devtools.require("devtools/styleinspector/rule-view");
let {CssLogic, CssSelector} = devtools.require("devtools/styleinspector/css-logic");
let promise = devtools.require("sdk/core/promise");
let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
gDevTools.testing = true;
SimpleTest.registerCleanupFunction(() => {

View File

@ -8,3 +8,6 @@
<!ENTITY newtab.undo.undoButton "Undo.">
<!ENTITY newtab.undo.restoreButton "Restore All.">
<!ENTITY newtab.undo.closeTooltip "Hide">
<!ENTITY newtab.panel.message "This site is being suggested because it has sponsored Mozilla, helping us promote openness, innovation and opportunity on the Web.">
<!ENTITY newtab.panel.link.url "https://support.mozilla.org/kb/how-do-sponsored-tiles-work">
<!ENTITY newtab.panel.link.text "Learn more…">

View File

@ -176,7 +176,6 @@ browser.jar:
* skin/classic/browser/devtools/common.css (../shared/devtools/common.css)
* skin/classic/browser/devtools/dark-theme.css (../shared/devtools/dark-theme.css)
* skin/classic/browser/devtools/light-theme.css (../shared/devtools/light-theme.css)
skin/classic/browser/devtools/metal.jpg (../shared/devtools/images/metal.jpg)
skin/classic/browser/devtools/filters.svg (../shared/devtools/filters.svg)
skin/classic/browser/devtools/controls.png (../shared/devtools/images/controls.png)
skin/classic/browser/devtools/controls@2x.png (../shared/devtools/images/controls@2x.png)

View File

@ -183,3 +183,7 @@
.newtab-control-sponsored:hover {
background-position: -265px -1px;
}
.newtab-control-sponsored[panelShown] {
background-position: -281px -1px;
}

View File

@ -2765,8 +2765,8 @@ toolbarbutton.chevron > .toolbarbutton-menu-dropmarker {
content: '';
display: block;
-moz-appearance: toolbar;
height: calc(@tabHeight@ + 1px);
margin-bottom: calc(-1px - @tabHeight@);
height: @tabHeight@;
margin-bottom: -@tabHeight@;
visibility: hidden;
}
@ -3901,19 +3901,17 @@ toolbarbutton.chevron > .toolbarbutton-menu-dropmarker {
/* Lion Fullscreen window styling */
@media (-moz-mac-lion-theme) {
#navigator-toolbox[inFullscreen]:not(:-moz-lwtheme)::before {
height: calc(@tabHeight@ + 11px) !important;
/* Adjust by the full element height of #titlebar, since that element is
* not displayed in native full-screen.
* Also add the height of the tabs, since we're calculating the
* total height of this pseudo-element, not just the top-padding. */
height: calc(@tabHeight@ + @spaceAboveTabbar@) !important;
}
#main-window[inFullscreen][privatebrowsingmode=temporary],
#main-window[inFullscreen]:-moz-lwtheme {
/* This additional padding matches the change in height in the pseudo-element
* above. */
padding-top: 11px;
}
#main-window[inFullscreen]:not([privatebrowsingmode=temporary]):-moz-lwtheme {
/* In combination with the previous rule, forces the top 22px of the
* background image to be hidden, so the image doesn't jump around with
* the loss of the titlebar. */
background-position: right -11px;
/* Adjust by the full element height of #titlebar, since that element is
* not displayed in native full-screen. */
padding-top: @spaceAboveTabbar@;
}
}

View File

@ -297,7 +297,6 @@ browser.jar:
* skin/classic/browser/devtools/common.css (../shared/devtools/common.css)
* skin/classic/browser/devtools/dark-theme.css (../shared/devtools/dark-theme.css)
* skin/classic/browser/devtools/light-theme.css (../shared/devtools/light-theme.css)
skin/classic/browser/devtools/metal.jpg (../shared/devtools/images/metal.jpg)
skin/classic/browser/devtools/filters.svg (../shared/devtools/filters.svg)
skin/classic/browser/devtools/controls.png (../shared/devtools/images/controls.png)
skin/classic/browser/devtools/controls@2x.png (../shared/devtools/images/controls@2x.png)

View File

@ -194,3 +194,7 @@
.newtab-control-sponsored:hover {
background-position: -265px -1px;
}
.newtab-control-sponsored[panelShown] {
background-position: -281px -1px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

View File

@ -135,7 +135,6 @@
color: #585959;
background-color: #f0f1f2;
border-color: #aaa;
background-image: url("chrome://browser/skin/devtools/metal.jpg");
}
.ruleview-colorswatch,

View File

@ -15,7 +15,6 @@
padding: 4px 3px;
border-bottom-width: 1px;
border-bottom-style: solid;
font: 3mm "comic sans ms", Purisa, message-box !important;
}
.devtools-menulist,
@ -616,13 +615,11 @@
border: 0px solid;
border-bottom-width: 1px;
padding: 0;
font: 3mm "comic sans ms", Purisa, message-box !important;
}
.theme-light .devtools-tabbar {
box-shadow: 0 -2px 0 rgba(170,170,170,.1) inset;
background: #ebeced;
background-image: url("chrome://browser/skin/devtools/metal.jpg");
border-bottom-color: #aaa;
}

View File

@ -161,7 +161,7 @@
.theme-light #breadcrumb-separator-after,
.theme-light #breadcrumb-separator-before:after {
background: url("chrome://browser/skin/devtools/metal.jpg"); /* Toolbars */
background: #f0f1f2; /* Toolbars */
}
/* This chevron arrow cannot be replicated easily in CSS, so we are using

View File

@ -209,7 +209,6 @@ browser.jar:
* skin/classic/browser/devtools/common.css (../shared/devtools/common.css)
* skin/classic/browser/devtools/dark-theme.css (../shared/devtools/dark-theme.css)
* skin/classic/browser/devtools/light-theme.css (../shared/devtools/light-theme.css)
skin/classic/browser/devtools/metal.jpg (../shared/devtools/images/metal.jpg)
skin/classic/browser/devtools/filters.svg (../shared/devtools/filters.svg)
skin/classic/browser/devtools/controls.png (../shared/devtools/images/controls.png)
skin/classic/browser/devtools/controls@2x.png (../shared/devtools/images/controls@2x.png)
@ -558,7 +557,6 @@ browser.jar:
* skin/classic/aero/browser/devtools/common.css (../shared/devtools/common.css)
* skin/classic/aero/browser/devtools/dark-theme.css (../shared/devtools/dark-theme.css)
* skin/classic/aero/browser/devtools/light-theme.css (../shared/devtools/light-theme.css)
skin/classic/aero/browser/devtools/metal.jpg (../shared/devtools/images/metal.jpg)
skin/classic/aero/browser/devtools/filters.svg (../shared/devtools/filters.svg)
skin/classic/aero/browser/devtools/controls.png (../shared/devtools/images/controls.png)
skin/classic/aero/browser/devtools/controls@2x.png (../shared/devtools/images/controls@2x.png)

View File

@ -186,3 +186,7 @@
.newtab-control-sponsored:hover {
background-position: -265px -1px;
}
.newtab-control-sponsored[panelShown] {
background-position: -281px -1px;
}

View File

@ -22,7 +22,7 @@ const Test = routine => () => {
// Returns promise for the observer notification subject for
// the given topic. If `receive("foo")` is called `n` times
// nth promise is resolved on an `nth` "foo" notification.
const receive = (topic, p) => {
const receive = (topic, p, syncCallback) => {
const { promise, resolve, reject } = Promise.defer();
const { queue } = receive;
const timeout = () => {
@ -43,6 +43,10 @@ const receive = (topic, p) => {
removeObserver(observer, topic);
clearTimeout(id, reject);
queue.splice(index, 2);
// Some tests need to be executed synchronously when the event is fired.
if (syncCallback) {
syncCallback(subject);
}
resolve(subject);
}
}
@ -73,7 +77,11 @@ const uri3 = "data:text/html;charset=utf-8,<h1>3</h1>";
const uri4 = "chrome://browser/content/license.html";
const test = Test(function*() {
let documentInteractive = receive("content-document-interactive", isData);
let documentInteractive = receive("content-document-interactive", isData, d => {
// This test is executed synchronously when the event is received.
is(d.readyState, "interactive", "document is interactive");
is(d.URL, uri1, "document.URL matches tab url");
});
let documentLoaded = receive("content-document-loaded", isData);
let pageShown = receive("content-page-shown", isData);
@ -82,8 +90,6 @@ const test = Test(function*() {
const browser1 = gBrowser.getBrowserForTab(tab1);
let interactiveDocument1 = yield documentInteractive;
is(interactiveDocument1.readyState, "interactive", "document is interactive");
is(interactiveDocument1.URL, uri1, "document.URL matches tab url");
let loadedDocument1 = yield documentLoaded;
is(loadedDocument1.readyState, "complete", "document is loaded");
@ -97,7 +103,11 @@ const test = Test(function*() {
info("load uri#2");
documentInteractive = receive("content-document-interactive", isData);
documentInteractive = receive("content-document-interactive", isData, d => {
// This test is executed synchronously when the event is received.
is(d.readyState, "interactive", "document is interactive");
is(d.URL, uri2, "document.URL matches URL loaded");
});
documentLoaded = receive("content-document-loaded", isData);
pageShown = receive("content-page-shown", isData);
let pageHidden = receive("content-page-hidden", isData);
@ -108,8 +118,6 @@ const test = Test(function*() {
is(interactiveDocument1, hiddenPage, "loaded document is hidden");
let interactiveDocument2 = yield documentInteractive;
is(interactiveDocument2.readyState, "interactive", "document is interactive");
is(interactiveDocument2.URL, uri2, "document.URL matches URL loaded");
let loadedDocument2 = yield documentLoaded;
is(loadedDocument2.readyState, "complete", "document is loaded");
@ -121,7 +129,11 @@ const test = Test(function*() {
info("go back to uri#1");
documentInteractive = receive("content-document-interactive", isData);
documentInteractive = receive("content-document-interactive", isData, d => {
// This test is executed synchronously when the event is received.
is(d.readyState, "interactive", "document is interactive");
is(d.URL, uri3, "document.URL matches URL loaded");
});
documentLoaded = receive("content-document-loaded", isData);
pageShown = receive("content-page-shown", isData);
pageHidden = receive("content-page-hidden", isData);
@ -139,8 +151,6 @@ const test = Test(function*() {
pageShown = receive("content-page-shown", isData);
let interactiveDocument3 = yield documentInteractive;
is(interactiveDocument3.readyState, "interactive", "document is interactive");
is(interactiveDocument3.URL, uri3, "document.URL matches URL loaded");
let loadedDocument3 = yield documentLoaded;
is(loadedDocument3.readyState, "complete", "document is loaded");
@ -154,13 +164,15 @@ const test = Test(function*() {
info("load chrome uri");
const tab2 = openTab(uri4);
documentInteractive = receive("chrome-document-interactive");
documentInteractive = receive("chrome-document-interactive", null, d => {
// This test is executed synchronously when the event is received.
is(d.readyState, "interactive", "document is interactive");
is(d.URL, uri4, "document.URL matches URL loaded");
});
documentLoaded = receive("chrome-document-loaded");
pageShown = receive("chrome-page-shown");
const interactiveDocument4 = yield documentInteractive;
is(interactiveDocument4.readyState, "interactive", "document is interactive");
is(interactiveDocument4.URL, uri4, "document.URL matches URL loaded");
let loadedDocument4 = yield documentLoaded;
is(loadedDocument4.readyState, "complete", "document is loaded");

View File

@ -2078,7 +2078,7 @@ abstract public class BrowserApp extends GeckoApp
// Action providers are available only ICS+.
if (Build.VERSION.SDK_INT >= 14) {
GeckoMenuItem share = (GeckoMenuItem) mMenu.findItem(R.id.share);
GeckoActionProvider provider = new GeckoActionProvider(this);
GeckoActionProvider provider = GeckoActionProvider.getForType(GeckoActionProvider.DEFAULT_MIME_TYPE, this);
share.setActionProvider(provider);
}

View File

@ -133,8 +133,7 @@ public class CrashReporter extends Activity
// Set the flag that indicates we were stopped as expected, as
// we will send a crash report, so it is not a silent OOM crash.
SharedPreferences prefs =
getSharedPreferences(GeckoApp.PREFS_NAME, 0);
SharedPreferences prefs = GeckoSharedPrefs.forApp(this);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(GeckoApp.PREFS_WAS_STOPPED, true);
editor.commit();
@ -231,8 +230,7 @@ public class CrashReporter extends Activity
}
private void savePrefs() {
SharedPreferences prefs = getSharedPreferences(GeckoApp.PREFS_NAME, 0);
SharedPreferences.Editor editor = prefs.edit();
SharedPreferences.Editor editor = GeckoSharedPrefs.forApp(this).edit();
final boolean allowContact = ((CheckBox) findViewById(R.id.allow_contact)).isChecked();
final boolean includeUrl = ((CheckBox) findViewById(R.id.include_url)).isChecked();

View File

@ -26,13 +26,12 @@ public class DataReportingNotification {
public static final String ALERT_NAME_DATAREPORTING_NOTIFICATION = "datareporting-notification";
private static final String DEFAULT_PREFS_BRANCH = AppConstants.ANDROID_PACKAGE_NAME + "_preferences";
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;
public static void checkAndNotifyPolicy(Context context) {
SharedPreferences dataPrefs = context.getSharedPreferences(DEFAULT_PREFS_BRANCH, 0);
SharedPreferences dataPrefs = GeckoSharedPrefs.forApp(context);
// Notify if user has not been notified or if policy version has changed.
if ((!dataPrefs.contains(PREFS_POLICY_NOTIFIED_TIME)) ||

View File

@ -34,8 +34,6 @@ import java.util.zip.ZipFile;
public final class Distribution {
private static final String LOGTAG = "GeckoDistribution";
private static final String DEFAULT_PREFS = GeckoApp.PREFS_NAME;
private static final int STATE_UNKNOWN = 0;
private static final int STATE_NONE = 1;
private static final int STATE_SET = 2;
@ -108,7 +106,7 @@ public final class Distribution {
* package path.
*/
public static void init(final Context context) {
Distribution.init(context, context.getPackageResourcePath(), DEFAULT_PREFS);
Distribution.init(context, context.getPackageResourcePath(), null);
}
/**
@ -137,7 +135,7 @@ public final class Distribution {
}
public Distribution(final Context context) {
this(context, context.getPackageResourcePath(), DEFAULT_PREFS);
this(context, context.getPackageResourcePath(), null);
}
/**
@ -148,7 +146,13 @@ public final class Distribution {
private boolean doInit() {
// Bail if we've already tried to initialize the distribution, and
// there wasn't one.
SharedPreferences settings = context.getSharedPreferences(prefsBranch, Activity.MODE_PRIVATE);
final SharedPreferences settings;
if (prefsBranch == null) {
settings = GeckoSharedPrefs.forApp(context);
} else {
settings = context.getSharedPreferences(prefsBranch, Activity.MODE_PRIVATE);
}
String keyName = context.getPackageName() + ".distribution_state";
this.state = settings.getInt(keyName, STATE_UNKNOWN);
if (this.state == STATE_NONE) {

View File

@ -86,7 +86,6 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.PowerManager;
import android.os.StrictMode;
import android.preference.PreferenceManager;
import android.provider.ContactsContract;
import android.provider.MediaStore.Images.Media;
import android.telephony.CellLocation;
@ -156,7 +155,6 @@ public abstract class GeckoApp
public static final String PREFS_ALLOW_STATE_BUNDLE = "allowStateBundle";
public static final String PREFS_CRASHED = "crashed";
public static final String PREFS_NAME = "GeckoApp";
public static final String PREFS_OOM_EXCEPTION = "OOMException";
public static final String PREFS_VERSION_CODE = "versionCode";
public static final String PREFS_WAS_STOPPED = "wasStopped";
@ -177,7 +175,7 @@ public abstract class GeckoApp
private View mCameraView;
private OrientationEventListener mCameraOrientationEventListener;
public List<GeckoAppShell.AppStateListener> mAppStateListeners;
private static GeckoApp sAppContext;
private static volatile GeckoApp sAppContext;
protected MenuPanel mMenuPanel;
protected Menu mMenu;
protected GeckoProfile mProfile;
@ -245,7 +243,11 @@ public abstract class GeckoApp
}
public static SharedPreferences getAppSharedPreferences() {
return GeckoApp.sAppContext.getSharedPreferences(GeckoApp.PREFS_NAME, 0);
if (sAppContext == null) {
return null;
}
return GeckoSharedPrefs.forApp(sAppContext);
}
public Activity getActivity() {
@ -1282,7 +1284,7 @@ public abstract class GeckoApp
// only intended to be used internally via Robocop, so a boolean
// is read from a private shared pref to prevent other apps from
// injecting states.
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
SharedPreferences prefs = getAppSharedPreferences();
if (prefs.getBoolean(PREFS_ALLOW_STATE_BUNDLE, false)) {
Log.i(LOGTAG, "Restoring state from intent bundle");
prefs.edit().remove(PREFS_ALLOW_STATE_BUNDLE).commit();
@ -1768,8 +1770,7 @@ public abstract class GeckoApp
}
private String getSessionRestorePreference() {
return PreferenceManager.getDefaultSharedPreferences(this)
.getString(GeckoPreferences.PREFS_RESTORE_SESSION, "quit");
return getAppSharedPreferences().getString(GeckoPreferences.PREFS_RESTORE_SESSION, "quit");
}
private boolean getRestartFromIntent() {

View File

@ -30,6 +30,7 @@ public final class GeckoProfile {
// Used to "lock" the guest profile, so that we'll always restart in it
private static final String LOCK_FILE_NAME = ".active_lock";
public static final String DEFAULT_PROFILE = "default";
private static final String GUEST_PROFILE = "guest";
private static HashMap<String, GeckoProfile> sProfileCache = new HashMap<String, GeckoProfile>();
private static String sDefaultProfileName = null;
@ -152,12 +153,21 @@ public final class GeckoProfile {
}
public static boolean removeProfile(Context context, String profileName) {
final boolean success;
try {
return new GeckoProfile(context, profileName).remove();
success = new GeckoProfile(context, profileName).remove();
} catch (NoMozillaDirectoryException e) {
Log.w(LOGTAG, "Unable to remove profile: no Mozilla directory.", e);
return true;
}
if (success) {
// Clear all shared prefs for the given profile.
GeckoSharedPrefs.forProfileName(context, profileName)
.edit().clear().commit();
}
return success;
}
public static GeckoProfile createGuestProfile(Context context) {
@ -193,7 +203,7 @@ public final class GeckoProfile {
if (sGuestProfile == null) {
File guestDir = getGuestDir(context);
if (guestDir.exists()) {
sGuestProfile = get(context, "guest", guestDir);
sGuestProfile = get(context, GUEST_PROFILE, guestDir);
sGuestProfile.mInGuestMode = true;
}
}
@ -218,14 +228,21 @@ public final class GeckoProfile {
}
private static void removeGuestProfile(Context context) {
boolean success = false;
try {
File guestDir = getGuestDir(context);
if (guestDir.exists()) {
delete(guestDir);
success = delete(guestDir);
}
} catch (Exception ex) {
Log.e(LOGTAG, "Error removing guest profile", ex);
}
if (success) {
// Clear all shared prefs for the guest profile.
GeckoSharedPrefs.forProfileName(context, GUEST_PROFILE)
.edit().clear().commit();
}
}
public static boolean delete(File file) throws IOException {

View File

@ -0,0 +1,284 @@
/* 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;
import org.mozilla.gecko.mozglue.RobocopTarget;
import org.mozilla.gecko.util.ThreadUtils;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Build;
import android.preference.PreferenceManager;
import android.util.Log;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* {@code GeckoSharedPrefs} provides scoped SharedPreferences instances.
* You should use this API instead of using Context.getSharedPreferences()
* directly. There are three methods to get scoped SharedPreferences instances:
*
* forApp()
* Use it for app-wide, cross-profile pref keys.
* forProfile()
* Use it to fetch and store keys for the current profile.
* forProfileName()
* Use it to fetch and store keys from/for a specific profile.
*
* {@code GeckoSharedPrefs} has a notion of migrations. Migrations can used to
* migrate keys from one scope to another. You can trigger a new migration by
* incrementing PREFS_VERSION and updating migrateIfNecessary() accordingly.
*
* Migration history:
* 1: Move all PreferenceManager keys to app/profile scopes
*/
@RobocopTarget
public final class GeckoSharedPrefs {
private static final String LOGTAG = "GeckoSharedPrefs";
// Increment it to trigger a new migration
public static final int PREFS_VERSION = 1;
// Name for app-scoped prefs
public static final String APP_PREFS_NAME = "GeckoApp";
// The prefs key that holds the current migration
private static final String PREFS_VERSION_KEY = "gecko_shared_prefs_migration";
// For disabling migration when getting a SharedPreferences instance
private static final EnumSet<Flags> disableMigrations = EnumSet.of(Flags.DISABLE_MIGRATIONS);
// Timeout for migration commits to be done (10 seconds)
private static final int MIGRATION_COMMIT_TIMEOUT_MSEC = 10000;
// The keys that have to be moved from ProfileManager's default
// shared prefs to the profile from version 0 to 1.
private static final String[] PROFILE_MIGRATIONS_0_TO_1 = {
"home_panels",
"home_locale"
};
// For optimizing the migration check in subsequent get() calls
private static volatile boolean migrationDone = false;
public enum Flags {
DISABLE_MIGRATIONS
}
// Used when fetching profile-scoped prefs.
private static final String PROFILE_PREFS_NAME_PREFIX = "GeckoProfile-";
public static SharedPreferences forApp(Context context) {
return forApp(context, EnumSet.noneOf(Flags.class));
}
/**
* Returns an app-scoped SharedPreferences instance. You can disable
* migrations by using the DISABLE_MIGRATIONS flag.
*/
public static SharedPreferences forApp(Context context, EnumSet<Flags> flags) {
if (flags != null && !flags.contains(Flags.DISABLE_MIGRATIONS)) {
migrateIfNecessary(context);
}
return context.getSharedPreferences(APP_PREFS_NAME, 0);
}
public static SharedPreferences forProfile(Context context) {
return forProfile(context, EnumSet.noneOf(Flags.class));
}
/**
* Returns a SharedPreferences instance scoped to the current profile
* in the app. You can disable migrations by using the DISABLE_MIGRATIONS
* flag.
*/
public static SharedPreferences forProfile(Context context, EnumSet<Flags> flags) {
String profileName = GeckoProfile.get(context).getName();
if (profileName == null) {
throw new IllegalStateException("Could not get current profile name");
}
return forProfileName(context, profileName, flags);
}
public static SharedPreferences forProfileName(Context context, String profileName) {
return forProfileName(context, profileName, EnumSet.noneOf(Flags.class));
}
/**
* Returns an SharedPreferences instance scoped to the given profile name.
* You can disable migrations by using the DISABLE_MIGRATION flag.
*/
public static SharedPreferences forProfileName(Context context, String profileName,
EnumSet<Flags> flags) {
if (flags != null && !flags.contains(Flags.DISABLE_MIGRATIONS)) {
migrateIfNecessary(context);
}
final String prefsName = PROFILE_PREFS_NAME_PREFIX + profileName;
return context.getSharedPreferences(prefsName, 0);
}
/**
* Returns the current version of the prefs.
*/
public static int getVersion(Context context) {
return forApp(context, disableMigrations).getInt(PREFS_VERSION_KEY, 0);
}
/**
* Resets migration flag. Should only be used in tests.
*/
public static synchronized void reset() {
migrationDone = false;
}
/**
* Performs all prefs migrations in the background thread to avoid StrictMode
* exceptions from reading/writing in the UI thread. This method will block
* the current thread until the migration is finished.
*/
private static synchronized void migrateIfNecessary(final Context context) {
if (migrationDone) {
return;
}
if (ThreadUtils.isOnBackgroundThread()) {
Log.d(LOGTAG, "Already in background thread, migrating directly");
performMigration(context);
} else {
Log.d(LOGTAG, "Not in background thread, migrating with lock");
final Object migrationLock = new Object();
ThreadUtils.getBackgroundHandler().post(new Runnable() {
@Override
public void run() {
synchronized(migrationLock) {
performMigration(context);
migrationLock.notifyAll();
}
}
});
try {
synchronized(migrationLock) {
migrationLock.wait(MIGRATION_COMMIT_TIMEOUT_MSEC);
}
} catch (InterruptedException e) {
throw new IllegalStateException("Failed to commit migration before timeout");
}
}
migrationDone = true;
}
private static void performMigration(Context context) {
final SharedPreferences appPrefs = forApp(context, disableMigrations);
final int currentVersion = appPrefs.getInt(PREFS_VERSION_KEY, 0);
Log.d(LOGTAG, "Current version = " + currentVersion + ", prefs version = " + PREFS_VERSION);
if (currentVersion == PREFS_VERSION) {
return;
}
Log.d(LOGTAG, "Performing migration");
final Editor appEditor = appPrefs.edit();
// The migration always moves prefs to the default profile, not
// the current one. We might have to revisit this if we ever support
// multiple profiles.
final String defaultProfileName;
try {
defaultProfileName = GeckoProfile.getDefaultProfileName(context);
} catch (Exception e) {
throw new IllegalStateException("Failed to get default profile name for migration");
}
final Editor profileEditor = forProfileName(context, defaultProfileName, disableMigrations).edit();
List<String> profileKeys;
Editor pmEditor = null;
for (int v = currentVersion + 1; v <= PREFS_VERSION; v++) {
Log.d(LOGTAG, "Migrating to version = " + v);
switch (v) {
case 1:
profileKeys = Arrays.asList(PROFILE_MIGRATIONS_0_TO_1);
pmEditor = migrateFromPreferenceManager(context, appEditor, profileEditor, profileKeys);
break;
}
}
// Update prefs version accordingly.
appEditor.putInt(PREFS_VERSION_KEY, PREFS_VERSION);
appEditor.commit();
profileEditor.commit();
if (pmEditor != null) {
pmEditor.commit();
}
Log.d(LOGTAG, "All keys have been migrated");
}
/**
* Moves all preferences stored in PreferenceManager's default prefs
* to either app or profile scopes. The profile-scoped keys are defined
* in given profileKeys list, all other keys are moved to the app scope.
*/
public static Editor migrateFromPreferenceManager(Context context, Editor appEditor,
Editor profileEditor, List<String> profileKeys) {
Log.d(LOGTAG, "Migrating from PreferenceManager");
final SharedPreferences pmPrefs =
PreferenceManager.getDefaultSharedPreferences(context);
for (Map.Entry<String, ?> entry : pmPrefs.getAll().entrySet()) {
final String key = entry.getKey();
final Editor to;
if (profileKeys.contains(key)) {
to = profileEditor;
} else {
to = appEditor;
}
putEntry(to, key, entry.getValue());
}
// Clear PreferenceManager's prefs once we're done
// and return the Editor to be committed.
return pmPrefs.edit().clear();
}
@SuppressWarnings("unchecked")
private static void putEntry(Editor to, String key, Object value) {
Log.d(LOGTAG, "Migrating key = " + key + " with value = " + value);
if (value instanceof String) {
to.putString(key, (String) value);
} else if (value instanceof Boolean) {
to.putBoolean(key, (Boolean) value);
} else if (value instanceof Long) {
to.putLong(key, (Long) value);
} else if (value instanceof Float) {
to.putFloat(key, (Float) value);
} else if (value instanceof Integer) {
to.putInt(key, (Integer) value);
} else {
throw new IllegalStateException("Unrecognized value type for key: " + key);
}
}
}

View File

@ -14,7 +14,6 @@ import org.json.JSONObject;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Log;
import java.util.Map;
@ -63,7 +62,7 @@ public final class SharedPreferencesHelper
private SharedPreferences getSharedPreferences(String branch) {
if (branch == null) {
return PreferenceManager.getDefaultSharedPreferences(mContext);
return GeckoSharedPrefs.forApp(mContext);
} else {
return mContext.getSharedPreferences(branch, Context.MODE_PRIVATE);
}

View File

@ -108,16 +108,23 @@ public class Telemetry {
}
public static void startUISession(String sessionName) {
Log.d(LOGTAG, "StartUISession: " + sessionName);
GeckoEvent event = GeckoEvent.createTelemetryUISessionStartEvent(sessionName, realtime());
GeckoAppShell.sendEventToGecko(event);
}
public static void stopUISession(String sessionName, String reason) {
Log.d(LOGTAG, "StopUISession: " + sessionName + ", reason=" + reason);
GeckoEvent event = GeckoEvent.createTelemetryUISessionStopEvent(sessionName, reason, realtime());
GeckoAppShell.sendEventToGecko(event);
}
public static void stopUISession(String sessionName) {
stopUISession(sessionName, null);
}
public static void sendUIEvent(String action, String method, long timestamp, String extras) {
Log.d(LOGTAG, "SendUIEvent: action = " + action + " method = " + method + " timestamp = " + timestamp + " extras = " + extras);
GeckoEvent event = GeckoEvent.createTelemetryUIEvent(action, method, timestamp, extras);
GeckoAppShell.sendEventToGecko(event);
}

View 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;
/**
* Holds data definitions for our UI Telemetry implementation.
*/
public interface TelemetryContract {
/**
* Holds event names. Intended for use with
* Telemetry.sendUIEvent() as the "action" parameter.
*/
public interface Event {}
/**
* Holds event methods. Intended for use in
* Telemetry.sendUIEvent() as the "method" parameter.
*/
public interface Method {}
/**
* Holds session names. Intended for use with
* Telemetry.startUISession() as the "sessionName" parameter.
*/
public interface Session {
// Started when a user enters about:home.
public static final String HOME = "home.1";
// Started when a user enters a given home panel.
// Session name is dynamic, encoded as "homepanel.1:<panel_id>"
public static final String HOME_PANEL = "homepanel.1:";
}
/**
* Holds reasons for stopping a session. Intended for use in
* Telemetry.stopUISession() as the "reason" parameter.
*/
public interface Reason {}
}

View File

@ -36,16 +36,18 @@ allows us to figure out how much time users are spending in the bookmarks
panel.
To start a session, call ``Telemetry.startUISession(String sessionName)``.
Session names should be brief, lowercase, and should describe which UI
component the user is interacting with. In certain cases where the UI component
is dynamic, they could include an ID, essential to identifying that component.
An example of this is dynamic home panels: we use session names of the format
``homepanel:<panel_id>`` to identify home panel sessions.
To stop a session call ``Telemetry.stopUISession(String sessionName, String
reason)``. ``sessionName`` is the name of the open session and ``reason`` is a
descriptive cause for the ending of the session. It should be brief, lowercase,
and generic so it can be reused in different places. Examples reasons are:
``sessionName``
The name of the session. Session names should be brief, lowercase, and should describe which UI
component the user is interacting with. In certain cases where the UI component is dynamic, they could include an ID, essential to identifying that component. An example of this is dynamic home panels: we use session names of the format ``homepanel:<panel_id>`` to identify home panel sessions.
To stop a session, call ``Telemetry.stopUISession(String sessionName, String reason)``.
``sessionName``
The name of the open session
``reason`` (Optional)
A descriptive cause for ending the session. It should be brief, lowercase, and generic so it can be reused in different places. Examples reasons are:
``switched``
The user transitioned to a UI element of equal level.
@ -53,7 +55,6 @@ and generic so it can be reused in different places. Examples reasons are:
``exit``
The user left for an entirely different element.
Events
------
@ -71,6 +72,11 @@ Events capture key occurrences. They should be brief and simple, and should not
``timestamp`` (Optional)
The time at which the event occurred. If not specified, this field defaults to the current value of the realtime clock.
Versioning
----------
As a we improve on our Telemetry methods, it is foreseeable that our probes will change over time. Different versions of a probe could carry different data or have different interpretations on the server-side. To make it easier for the server to handle these changes, you should add version numbers to your event and session names. An example of a versioned session is ``homepanel.1``; this is version 1 of the ``homepanel`` session. This approach should also be applied to event names, an example being: ``panel.enable.1`` and ``panel.enable.2``.
Clock
-----

View File

@ -15,6 +15,7 @@ import java.util.Locale;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.home.HomeConfig.HomeConfigBackend;
import org.mozilla.gecko.home.HomeConfig.OnReloadListener;
import org.mozilla.gecko.home.HomeConfig.PanelConfig;
@ -28,7 +29,6 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.preference.PreferenceManager;
import android.support.v4.content.LocalBroadcastManager;
import android.text.TextUtils;
import android.util.Log;
@ -50,7 +50,7 @@ class HomeConfigPrefsBackend implements HomeConfigBackend {
}
private SharedPreferences getSharedPreferences() {
return PreferenceManager.getDefaultSharedPreferences(mContext);
return GeckoSharedPrefs.forProfile(mContext);
}
private State loadDefaultConfig() {

View File

@ -10,6 +10,8 @@ import java.util.EnumSet;
import java.util.List;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.animation.PropertyAnimator;
import org.mozilla.gecko.animation.ViewHelper;
import org.mozilla.gecko.home.HomeAdapter.OnAddPanelListener;
@ -30,7 +32,6 @@ import android.view.View;
import android.view.ViewGroup;
public class HomePager extends ViewPager {
private static final int LOADER_ID_CONFIG = 0;
private final Context mContext;
@ -50,6 +51,9 @@ public class HomePager extends ViewPager {
// Cached original ViewPager background.
private final Drawable mOriginalBackground;
// Telemetry session for current panel.
private String mCurrentPanelSession;
// This is mostly used by UI tests to easily fetch
// specific list views at runtime.
static final String LIST_TAG_HISTORY = "history";
@ -195,6 +199,7 @@ public class HomePager extends ViewPager {
PropertyAnimator.Property.ALPHA,
1.0f);
}
Telemetry.startUISession(TelemetryContract.Session.HOME);
}
/**
@ -203,6 +208,10 @@ public class HomePager extends ViewPager {
public void unload() {
mLoaded = false;
setAdapter(null);
// Stop UI Telemetry sessions.
stopCurrentPanelTelemetrySession();
Telemetry.stopUISession(TelemetryContract.Session.HOME);
}
/**
@ -361,6 +370,10 @@ public class HomePager extends ViewPager {
if (mHomeBanner != null) {
mHomeBanner.setActive(position == mDefaultPageIndex);
}
// Start a UI telemetry session for the newly selected panel.
final String newPanelId = ((HomeAdapter) getAdapter()).getPanelIdAtPosition(position);
startNewPanelTelemetrySession(newPanelId);
}
@Override
@ -377,4 +390,29 @@ public class HomePager extends ViewPager {
@Override
public void onPageScrollStateChanged(int state) { }
}
/**
* Start UI telemetry session for the a panel.
* If there is currently a session open for a panel,
* it will be stopped before a new one is started.
*
* @param panelId of panel to start a session for
*/
private void startNewPanelTelemetrySession(String panelId) {
// Stop the current panel's session if we have one.
stopCurrentPanelTelemetrySession();
mCurrentPanelSession = TelemetryContract.Session.HOME_PANEL + panelId;
Telemetry.startUISession(mCurrentPanelSession);
}
/**
* Stop the current panel telemetry session if one exists.
*/
private void stopCurrentPanelTelemetrySession() {
if (mCurrentPanelSession != null) {
Telemetry.stopUISession(mCurrentPanelSession);
mCurrentPanelSession = null;
}
}
}

View File

@ -8,9 +8,10 @@ package org.mozilla.gecko.home;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.preference.PreferenceManager;
import android.util.Log;
import org.mozilla.gecko.GeckoSharedPrefs;
/**
* Cache used to store authentication state of dynamic panels. The values
* in this cache are set in JS through the Home.panels API.
@ -38,7 +39,7 @@ class PanelAuthCache {
}
private SharedPreferences getSharedPreferences() {
return PreferenceManager.getDefaultSharedPreferences(mContext);
return GeckoSharedPrefs.forProfile(mContext);
}
private String getPanelAuthKey(String panelId) {

View File

@ -172,6 +172,7 @@ gbjar.sources += [
'GeckoProfileDirectories.java',
'GeckoProfilesProvider.java',
'GeckoScreenOrientation.java',
'GeckoSharedPrefs.java',
'GeckoSmsManager.java',
'GeckoThread.java',
'GeckoUpdateReceiver.java',
@ -347,6 +348,7 @@ gbjar.sources += [
'TabsPanel.java',
'TabsTray.java',
'Telemetry.java',
'TelemetryContract.java',
'TextSelection.java',
'TextSelectionHandle.java',
'ThumbnailHelper.java',

View File

@ -17,6 +17,7 @@ import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.GeckoEvent;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.PrefsHelper;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.announcements.AnnouncementsConstants;
@ -45,7 +46,6 @@ import android.preference.Preference.OnPreferenceChangeListener;
import android.preference.Preference.OnPreferenceClickListener;
import android.preference.PreferenceActivity;
import android.preference.PreferenceGroup;
import android.preference.PreferenceManager;
import android.preference.PreferenceScreen;
import android.preference.TwoStatePreference;
import android.text.Editable;
@ -502,7 +502,7 @@ public class GeckoPreferences
final boolean value) {
final Intent intent = new Intent(action)
.putExtra("pref", pref)
.putExtra("branch", GeckoApp.PREFS_NAME)
.putExtra("branch", GeckoSharedPrefs.APP_PREFS_NAME)
.putExtra("enabled", value);
broadcastAction(context, intent);
}
@ -577,7 +577,7 @@ public class GeckoPreferences
* @return the value of the preference, or the default.
*/
public static boolean getBooleanPref(final Context context, final String name, boolean def) {
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
final SharedPreferences prefs = GeckoSharedPrefs.forApp(context);
return prefs.getBoolean(name, def);
}

View File

@ -6,6 +6,7 @@
package org.mozilla.gecko.preferences;
import org.mozilla.gecko.R;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.util.ThreadUtils;
import android.app.AlertDialog;
@ -14,7 +15,6 @@ import android.content.Context;
import android.content.DialogInterface;
import android.content.res.TypedArray;
import android.preference.DialogPreference;
import android.preference.PreferenceManager;
import android.util.AttributeSet;
import android.widget.Button;
@ -205,7 +205,7 @@ class MultiChoicePreference extends DialogPreference {
return true;
}
PreferenceManager.getDefaultSharedPreferences(getContext())
GeckoSharedPrefs.forApp(getContext())
.edit().putBoolean(key, value).commit();
return true;
}
@ -216,7 +216,7 @@ class MultiChoicePreference extends DialogPreference {
if (!isPersistent())
return defaultReturnValue;
return PreferenceManager.getDefaultSharedPreferences(getContext())
return GeckoSharedPrefs.forApp(getContext())
.getBoolean(key, defaultReturnValue);
}

View File

@ -2,6 +2,7 @@ package org.mozilla.gecko.prompts;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.widget.GeckoActionProvider;
import org.json.JSONArray;
import org.json.JSONObject;
@ -41,7 +42,8 @@ public class PromptListItem {
if (obj != null) {
showAsActions = true;
String uri = obj.isNull("uri") ? "" : obj.optString("uri");
String type = obj.isNull("type") ? "text/html" : obj.optString("type", "text/html");
String type = obj.isNull("type") ? GeckoActionProvider.DEFAULT_MIME_TYPE :
obj.optString("type", GeckoActionProvider.DEFAULT_MIME_TYPE);
mIntent = GeckoAppShell.getShareIntent(GeckoAppShell.getContext(), uri, type, "");
isParent = true;
} else {

View File

@ -247,7 +247,7 @@ public class testSettingsMenuItems extends PixelTest {
mActions.sendSpecialKey(Actions.SpecialKey.BACK);
menuDepth--;
// Sleep so subsequent back actions aren't lost.
mSolo.sleep(50);
mSolo.sleep(150);
}
}
}

View File

@ -7,6 +7,10 @@ import org.mozilla.gecko.Telemetry;
import android.util.Log;
public class testUITelemetry extends JavascriptTest {
// Prefix used to distinguish test events and sessions from
// real ones. Used by the javascript part of the test.
static final String TEST_PREFIX = "TEST-";
public testUITelemetry() {
super("testUITelemetry.js");
}
@ -22,18 +26,18 @@ public class testUITelemetry extends JavascriptTest {
Log.i("GeckoTest", "Adding telemetry events.");
try {
Telemetry.sendUIEvent("enone", "method0");
Telemetry.startUISession("foo");
Telemetry.sendUIEvent("efoo", "method1");
Telemetry.startUISession("foo");
Telemetry.sendUIEvent("efoo", "method2");
Telemetry.startUISession("bar");
Telemetry.sendUIEvent("efoobar", "method3", "foobarextras");
Telemetry.stopUISession("foo", "reasonfoo");
Telemetry.sendUIEvent("ebar", "method4", "barextras");
Telemetry.stopUISession("bar", "reasonbar");
Telemetry.stopUISession("bar", "reasonbar2");
Telemetry.sendUIEvent("enone", "method5");
Telemetry.sendUIEvent(TEST_PREFIX + "enone", "method0");
Telemetry.startUISession(TEST_PREFIX + "foo");
Telemetry.sendUIEvent(TEST_PREFIX + "efoo", "method1");
Telemetry.startUISession(TEST_PREFIX + "foo");
Telemetry.sendUIEvent(TEST_PREFIX + "efoo", "method2");
Telemetry.startUISession(TEST_PREFIX + "bar");
Telemetry.sendUIEvent(TEST_PREFIX + "efoobar", "method3", "foobarextras");
Telemetry.stopUISession(TEST_PREFIX + "foo", "reasonfoo");
Telemetry.sendUIEvent(TEST_PREFIX + "ebar", "method4", "barextras");
Telemetry.stopUISession(TEST_PREFIX + "bar", "reasonbar");
Telemetry.stopUISession(TEST_PREFIX + "bar", "reasonbar2");
Telemetry.sendUIEvent(TEST_PREFIX + "enone", "method5");
} catch (Exception e) {
Log.e("GeckoTest", "Oops.", e);
}

View File

@ -9,6 +9,9 @@ const Cu = Components.utils;
Cu.import("resource://gre/modules/Services.jsm");
const TEST_PREFIX = "TEST-";
const TEST_REGEX = new RegExp("^" + TEST_PREFIX);
function do_check_array_eq(a1, a2) {
do_check_eq(a1.length, a2.length);
for (let i = 0; i < a1.length; ++i) {
@ -37,17 +40,21 @@ add_test(function test_enabled() {
add_test(function test_telemetry_events() {
let obs = getObserver();
let measurements = obs.getUIMeasurements();
let measurements = obs.getUIMeasurements().filter(function(m) {
// Only want events and sessions that were generated by
// the Java-side of the test.
return TEST_REGEX.test(m.type == "event" ? m.action : m.name);
});
let expected = [
["event", "enone", "method0", [], null],
["event", "efoo", "method1", ["foo"], null],
["event", "efoo", "method2", ["foo"], null],
["event", "efoobar", "method3", ["foo", "bar"], "foobarextras"],
["session", "foo", "reasonfoo"],
["event", "ebar", "method4", ["bar"], "barextras"],
["session", "bar", "reasonbar"],
["event", "enone", "method5", [], null],
["event", TEST_PREFIX + "enone", "method0", [], null],
["event", TEST_PREFIX + "efoo", "method1", ["foo"], null],
["event", TEST_PREFIX + "efoo", "method2", ["foo"], null],
["event", TEST_PREFIX + "efoobar", "method3", ["foo", "bar"], "foobarextras"],
["session", TEST_PREFIX + "foo", "reasonfoo"],
["event", TEST_PREFIX + "ebar", "method4", ["bar"], "barextras"],
["session", TEST_PREFIX + "bar", "reasonbar"],
["event", TEST_PREFIX + "enone", "method5", [], null],
];
do_check_eq(expected.length, measurements.length);

View File

@ -23,8 +23,8 @@ public final class ThreadUtils {
THROW,
}
private static Thread sUiThread;
private static Thread sBackgroundThread;
private static volatile Thread sUiThread;
private static volatile Thread sBackgroundThread;
private static Handler sUiHandler;
@ -158,6 +158,10 @@ public final class ThreadUtils {
}
public static boolean isOnBackgroundThread() {
if (sBackgroundThread == null) {
return false;
}
return isOnThread(sBackgroundThread);
}

View File

@ -9,8 +9,10 @@ import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.GeckoApp;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoEvent;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.util.ThreadUtils;
import android.app.ActivityManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
@ -52,15 +54,56 @@ public class UninstallListener extends BroadcastReceiver {
ArrayList<String> installedPackages = allocator.getInstalledPackageNames();
if (installedPackages.contains(packageName)) {
doUninstall(context, packageName);
}
}
private static void doUninstall(Context context, String packageName) {
ArrayList<String> uninstalledPackages = new ArrayList<String>();
uninstalledPackages.add(packageName);
doUninstall(context, uninstalledPackages);
}
private static void doUninstall(Context context, ArrayList<String> packageNames) {
Allocator allocator = Allocator.getInstance(context);
JSONObject message = new JSONObject();
JSONArray packageNames = new JSONArray();
JSONArray jsonPackages = new JSONArray();
for (String packageName : packageNames) {
// Although its unlikely that an app is not allocated, but is installed in Gecko, it
// is possible. We always send the packageName to JS to be removed from Gecko's registry.
jsonPackages.put(packageName);
int index = allocator.getIndexForApp(packageName);
// If -1, nothing more to do; we didn't think it was installed anyway.
if (index == -1)
continue;
// kill the app if it's running
String targetProcessName = context.getPackageName();
targetProcessName = targetProcessName + ":" + targetProcessName + ".Webapp" + index;
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningAppProcessInfo> procs = am.getRunningAppProcesses();
if (procs != null) {
for (ActivityManager.RunningAppProcessInfo proc : procs) {
if (proc.processName.equals(targetProcessName)) {
android.os.Process.killProcess(proc.pid);
break;
}
}
}
// then nuke the profile
GeckoProfile.removeProfile(context, "webapp" + index);
}
try {
packageNames.put(packageName);
message.put("apkPackageNames", packageNames);
message.put("apkPackageNames", jsonPackages);
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Webapps:AutoUninstall", message.toString()));
} catch (JSONException e) {
Log.e(LOGTAG, "JSON EXCEPTION " + e);
}
Log.e(LOGTAG, "Error sending uninstall packages to Gecko", e);
}
}
@ -76,7 +119,6 @@ public class UninstallListener extends BroadcastReceiver {
Set<String> allInstalledPackages = new HashSet<String>();
for (ApplicationInfo packageInfo : packages) {
//Log.i(LOGTAG, "Android package: " + packageInfo.packageName);
allInstalledPackages.add(packageInfo.packageName);
}
@ -87,17 +129,7 @@ public class UninstallListener extends BroadcastReceiver {
}
if (uninstalledPackages.size() > 0) {
JSONObject message = new JSONObject();
JSONArray packageNames = new JSONArray();
try {
for (String packageName : uninstalledPackages) {
packageNames.put(packageName);
}
message.put("apkPackageNames", packageNames);
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Webapps:AutoUninstall", message.toString()));
} catch (JSONException e) {
Log.e(LOGTAG, "JSON EXCEPTION " + e);
}
doUninstall(context, uninstalledPackages);
}
}

View File

@ -36,6 +36,8 @@ public class GeckoActionProvider {
private final Context mContext;
public final static String DEFAULT_MIME_TYPE = "text/plain";
public static final String DEFAULT_HISTORY_FILE_NAME = "history.xml";
// History file.

View File

@ -62,6 +62,7 @@ var SelectionHandler = {
BrowserApp.deck.addEventListener("pagehide", this, false);
BrowserApp.deck.addEventListener("blur", this, true);
BrowserApp.deck.addEventListener("scroll", this, true);
},
_removeObservers: function sh_removeObservers() {
@ -75,6 +76,7 @@ var SelectionHandler = {
BrowserApp.deck.removeEventListener("pagehide", this);
BrowserApp.deck.removeEventListener("blur", this);
BrowserApp.deck.removeEventListener("scroll", this);
},
observe: function sh_observe(aSubject, aTopic, aData) {
@ -201,6 +203,11 @@ var SelectionHandler = {
handleEvent: function sh_handleEvent(aEvent) {
switch (aEvent.type) {
case "scroll":
// Maintain position when top-level document is scrolled
this._positionHandles();
break;
case "pagehide":
case "blur":
this._closeSelection();

View File

@ -11,6 +11,7 @@ jar.sources += [
'src/harness/BrowserInstrumentationTestRunner.java',
'src/harness/BrowserTestListener.java',
'src/tests/BrowserTestCase.java',
'src/tests/TestGeckoSharedPrefs.java',
'src/tests/TestJarReader.java',
]
jar.generated_sources = [] # None yet -- try to keep it this way.

View File

@ -0,0 +1,155 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.browser.tests;
import java.util.Arrays;
import java.util.Collections;
import java.util.EnumSet;
import java.util.Map;
import java.util.HashSet;
import java.util.Set;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.GeckoSharedPrefs.Flags;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Build;
import android.preference.PreferenceManager;
import android.test.RenamingDelegatingContext;
/**
* Test GeckoSharedPrefs migrations.
*/
public class TestGeckoSharedPrefs extends BrowserTestCase {
private static class TestContext extends RenamingDelegatingContext {
private static final String PREFIX = "TestGeckoSharedPrefs-";
private final Set<String> usedPrefs;
public TestContext(Context context) {
super(context, PREFIX);
usedPrefs = Collections.synchronizedSet(new HashSet<String>());
}
@Override
public SharedPreferences getSharedPreferences(String name, int mode) {
usedPrefs.add(name);
return super.getSharedPreferences(PREFIX + name, mode);
}
public void clearUsedPrefs() {
for (String prefsName : usedPrefs) {
getSharedPreferences(prefsName, 0).edit().clear().commit();
}
usedPrefs.clear();
}
}
private static final EnumSet<Flags> disableMigrations = EnumSet.of(Flags.DISABLE_MIGRATIONS);
private TestContext context;
protected void setUp() {
context = new TestContext(getApplicationContext());
}
protected void tearDown() {
context.clearUsedPrefs();
GeckoSharedPrefs.reset();
}
public void testDisableMigrations() {
// Version is 0 before any migration
assertEquals(0, GeckoSharedPrefs.getVersion(context));
// Get prefs with migrations disabled
GeckoSharedPrefs.forApp(context, disableMigrations);
GeckoSharedPrefs.forProfile(context, disableMigrations);
GeckoSharedPrefs.forProfileName(context, "someProfile", disableMigrations);
// Version should still be 0
assertEquals(0, GeckoSharedPrefs.getVersion(context));
}
public void testPrefsVersion() {
// Version is 0 before any migration
assertEquals(0, GeckoSharedPrefs.getVersion(context));
// Trigger migration by getting a SharedPreferences instance
GeckoSharedPrefs.forApp(context);
// Version should be current after migration
assertEquals(GeckoSharedPrefs.PREFS_VERSION, GeckoSharedPrefs.getVersion(context));
}
public void testMigrateFromPreferenceManager() {
SharedPreferences appPrefs = GeckoSharedPrefs.forApp(context, disableMigrations);
assertTrue(appPrefs.getAll().isEmpty());
final Editor appEditor = appPrefs.edit();
SharedPreferences profilePrefs = GeckoSharedPrefs.forProfileName(context, GeckoProfile.DEFAULT_PROFILE, disableMigrations);
assertTrue(profilePrefs.getAll().isEmpty());
final Editor profileEditor = profilePrefs.edit();
final SharedPreferences pmPrefs = PreferenceManager.getDefaultSharedPreferences(context);
assertTrue(pmPrefs.getAll().isEmpty());
Editor pmEditor = pmPrefs.edit();
// Insert a key for each type to exercise the
// migration path a bit more thoroughly.
pmEditor.putInt("int_key", 23);
pmEditor.putLong("long_key", 23L);
pmEditor.putString("string_key", "23");
pmEditor.putFloat("float_key", 23.3f);
final String[] profileKeys = {
"string_profile",
"int_profile"
};
// Insert keys that are expected to be moved to the
// PROFILE scope.
pmEditor.putString(profileKeys[0], "24");
pmEditor.putInt(profileKeys[1], 24);
// Commit changes to PreferenceManager
pmEditor.commit();
assertEquals(6, pmPrefs.getAll().size());
// Perform actual migration with the given editors
pmEditor = GeckoSharedPrefs.migrateFromPreferenceManager(context, appEditor,
profileEditor, Arrays.asList(profileKeys));
// Commit changes applied during the migration
appEditor.commit();
profileEditor.commit();
pmEditor.commit();
// PreferenceManager should have no keys
assertTrue(pmPrefs.getAll().isEmpty());
// App should have all keys except the profile ones
assertEquals(4, appPrefs.getAll().size());
// Ensure app scope doesn't have any of the profile keys
for (int i = 0; i < profileKeys.length; i++) {
assertFalse(appPrefs.contains(profileKeys[i]));
}
// Check app keys
assertEquals(23, appPrefs.getInt("int_key", 0));
assertEquals(23L, appPrefs.getLong("long_key", 0L));
assertEquals("23", appPrefs.getString("string_key", ""));
assertEquals(23.3f, appPrefs.getFloat("float_key", 0));
assertEquals(2, profilePrefs.getAll().size());
assertEquals("24", profilePrefs.getString(profileKeys[0], ""));
assertEquals(24, profilePrefs.getInt(profileKeys[1], 0));
}
}

View File

@ -553,6 +553,9 @@ pref("toolkit.asyncshutdown.timeout.crash", 60000);
// Enable deprecation warnings.
pref("devtools.errorconsole.deprecation_warnings", true);
// Disable debugging chrome
pref("devtools.chrome.enabled", false);
// Disable remote debugging protocol logging
pref("devtools.debugger.log", false);
// Disable remote debugging connections

View File

@ -86,7 +86,7 @@ function Tester(aTests, aDumper, aCallback) {
this.SimpleTest = simpleTestScope.SimpleTest;
this.MemoryStats = simpleTestScope.MemoryStats;
this.Task = Components.utils.import("resource://gre/modules/Task.jsm", null).Task;
this.Promise = Components.utils.import("resource://gre/modules/commonjs/sdk/core/promise.js", null).Promise;
this.Promise = Components.utils.import("resource://gre/modules/Promise.jsm", null).Promise;
this.Assert = Components.utils.import("resource://testing-common/Assert.jsm", null).Assert;
}
Tester.prototype = {

View File

@ -11,7 +11,7 @@
<script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
<script type="application/javascript">
<![CDATA[
let {Promise} = Components.utils.import("resource://gre/modules/commonjs/sdk/core/promise.js");
let {Promise} = Components.utils.import("resource://gre/modules/Promise.jsm");
Components.utils.import("resource://gre/modules/osfile.jsm");
let decoder = new TextDecoder();

View File

@ -1,5 +1,5 @@
function test() {
let {Promise} = Components.utils.import("resource://gre/modules/commonjs/sdk/core/promise.js");
let {Promise} = Components.utils.import("resource://gre/modules/Promise.jsm");
Components.utils.import("resource://gre/modules/osfile.jsm");
let decoder = new TextDecoder();

View File

@ -124,7 +124,7 @@ const EXCEPTION_NAMES = {
} else if (exn.constructor.name in EXCEPTION_NAMES) {
LOG("Sending back exception", exn.constructor.name);
post({fail: {exn: exn.constructor.name, message: exn.message,
fileName: exn.fileName, lineNumber: exn.lineNumber},
fileName: exn.moduleName || exn.fileName, lineNumber: exn.lineNumber},
id: id, durationMs: durationMs});
} else {
// Other exceptions do not, and should be propagated through DOM's

View File

@ -3255,6 +3255,11 @@
"extended_statistics_ok": true,
"description": "Firefox: Time taken to load a page (ms)"
},
"FX_TOTAL_TOP_VISITS": {
"expires_in_version": "never",
"kind": "boolean",
"description": "Count the number of times a new top page was starting to load"
},
"FX_THUMBNAILS_CAPTURE_TIME_MS": {
"expires_in_version": "never",
"kind": "exponential",

View File

@ -5,6 +5,9 @@ const Cu = Components.utils;
Cu.import("resource://gre/modules/TelemetryLog.jsm", this);
Cu.import("resource://gre/modules/TelemetryPing.jsm", this);
const TEST_PREFIX = "TEST-";
const TEST_REGEX = new RegExp("^" + TEST_PREFIX);
function check_event(event, id, data)
{
do_print("Checking message " + id);
@ -24,15 +27,19 @@ function check_event(event, id, data)
function run_test()
{
TelemetryLog.log("test1", ["val", 123, undefined]);
TelemetryLog.log("test2", []);
TelemetryLog.log("test3");
TelemetryLog.log(TEST_PREFIX + "1", ["val", 123, undefined]);
TelemetryLog.log(TEST_PREFIX + "2", []);
TelemetryLog.log(TEST_PREFIX + "3");
var log = TelemetryPing.getPayload().log.filter(function(e) {
// Only want events that were generated by the test.
return TEST_REGEX.test(e[0]);
});
var log = TelemetryPing.getPayload().log;
do_check_eq(log.length, 3);
check_event(log[0], "test1", ["val", "123", "undefined"]);
check_event(log[1], "test2", []);
check_event(log[2], "test3", undefined);
check_event(log[0], TEST_PREFIX + "1", ["val", "123", "undefined"]);
check_event(log[1], TEST_PREFIX + "2", []);
check_event(log[2], TEST_PREFIX + "3", undefined);
do_check_true(log[0][1] <= log[1][1]);
do_check_true(log[1][1] <= log[2][1]);
}

View File

@ -93,7 +93,7 @@ function compressFileContent(array, options = {}) {
exports.compressFileContent = compressFileContent;
function decompressFileContent(array, options = {}) {
let {ptr, bytes} = SharedAll.normalizeToPointer(array, options.bytes);
let {ptr, bytes} = SharedAll.normalizeToPointer(array, options.bytes || null);
if (bytes < HEADER_SIZE) {
throw new LZError("decompress", "becauseLZNoHeader", "Buffer is too short (no header)");
}

View File

@ -186,12 +186,16 @@ function promise_window_close(aWindow) {
function promise_page(aWindow, aPageId) {
let deferred = Promise.defer();
var page = aWindow.document.getElementById(aPageId);
if (aWindow.document.getElementById("updateWizard").currentPage === page) {
deferred.resolve(aWindow);
} else {
page.addEventListener("pageshow", function() {
page.removeEventListener("pageshow", arguments.callee, false);
executeSoon(function() {
deferred.resolve(aWindow);
});
}, false);
}
return deferred.promise;
}