merge fx-team to mozilla-central

This commit is contained in:
Carsten "Tomcat" Book 2014-05-23 15:49:37 +02:00
commit ef87b38291
73 changed files with 2039 additions and 439 deletions

View File

@ -36,13 +36,20 @@ function checkPreferences(prefsWin) {
});
});
}
// Same as the other one, but for in-content preferences
function checkInContentPreferences(win) {
let sel = win.history.state;
let doc = win.document;
let tab = doc.getElementById("advancedPrefs").selectedTab.id;
is(gBrowser.currentURI.spec, "about:preferences", "about:preferences loaded");
is(sel, "paneAdvanced", "Advanced pane was selected");
is(tab, "networkTab", "Network tab is selected");
// all good, we are done.
win.close();
finish();
}
function test() {
if (Services.prefs.getBoolPref("browser.preferences.inContent")) {
// Bug 881576 - ensure this works with inContent prefs.
todo(false, "Bug 881576 - this test needs to be updated for inContent prefs");
return;
}
waitForExplicitFinish();
gBrowser.selectedBrowser.addEventListener("load", function onload() {
gBrowser.selectedBrowser.removeEventListener("load", onload, true);
@ -55,13 +62,22 @@ function test() {
// window to open - which we track either via a window watcher (for
// the window-based prefs) or via an "Initialized" event (for
// in-content prefs.)
Services.ww.registerNotification(function wwobserver(aSubject, aTopic, aData) {
if (aTopic != "domwindowopened")
return;
Services.ww.unregisterNotification(wwobserver);
checkPreferences(aSubject);
});
if (!Services.prefs.getBoolPref("browser.preferences.inContent")) {
Services.ww.registerNotification(function wwobserver(aSubject, aTopic, aData) {
if (aTopic != "domwindowopened")
return;
Services.ww.unregisterNotification(wwobserver);
checkPreferences(aSubject);
});
}
PopupNotifications.panel.firstElementChild.button.click();
if (Services.prefs.getBoolPref("browser.preferences.inContent")) {
let newTabBrowser = gBrowser.getBrowserForTab(gBrowser.selectedTab);
newTabBrowser.addEventListener("Initialized", function PrefInit() {
newTabBrowser.removeEventListener("Initialized", PrefInit, true);
checkInContentPreferences(newTabBrowser.contentWindow);
}, true);
}
});
};
Services.prefs.setIntPref("offline-apps.quota.warn", 1);

View File

@ -100,6 +100,25 @@ function fillSubviewFromMenuItems(aMenuItems, aSubview) {
} else if (menuChild.localName == "menuitem") {
subviewItem = doc.createElementNS(kNSXUL, "toolbarbutton");
CustomizableUI.addShortcut(menuChild, subviewItem);
let item = menuChild;
if (!item.hasAttribute("onclick")) {
subviewItem.addEventListener("click", event => {
let newEvent = new doc.defaultView.MouseEvent(event.type, event);
item.dispatchEvent(newEvent);
});
}
if (!item.hasAttribute("oncommand")) {
subviewItem.addEventListener("command", event => {
let newEvent = doc.createEvent("XULCommandEvent");
newEvent.initCommandEvent(
event.type, event.bubbles, event.cancelable, event.view,
event.detail, event.ctrlKey, event.altKey, event.shiftKey,
event.metaKey, event.sourceEvent);
item.dispatchEvent(newEvent);
});
}
} else {
continue;
}
@ -937,4 +956,4 @@ if (Services.prefs.getBoolPref("browser.tabs.remote")) {
onCommand: getCommandFunction(openRemote),
});
}
#endif
#endif

View File

@ -99,6 +99,7 @@ skip-if = os == "linux"
[browser_985815_propagate_setToolbarVisibility.js]
[browser_981305_separator_insertion.js]
[browser_988072_sidebar_events.js]
[browser_989751_subviewbutton_class.js]
[browser_987177_destroyWidget_xul.js]
[browser_987177_xul_wrapper_updating.js]

View File

@ -0,0 +1,371 @@
/* 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";
let gSidebarMenu = document.getElementById("viewSidebarMenu");
let gTestSidebarItem = null;
let EVENTS = {
click: 0, command: 0,
onclick: 0, oncommand: 0
};
window.sawEvent = function(event, isattr) {
let type = (isattr ? "on" : "") + event.type
EVENTS[type]++;
};
registerCleanupFunction(() => {
delete window.sawEvent;
});
function checkExpectedEvents(expected) {
for (let type of Object.keys(EVENTS)) {
let count = (type in expected ? expected[type] : 0);
is(EVENTS[type], count, "Should have seen the right number of " + type + " events");
EVENTS[type] = 0;
}
}
function createSidebarItem() {
gTestSidebarItem = document.createElement("menuitem");
gTestSidebarItem.id = "testsidebar";
gTestSidebarItem.setAttribute("label", "Test Sidebar");
gSidebarMenu.insertBefore(gTestSidebarItem, gSidebarMenu.firstChild);
}
// Filters out the trailing menuseparators from the sidebar list
function getSidebarList() {
let sidebars = [...gSidebarMenu.children];
while (sidebars[sidebars.length - 1].localName == "menuseparator")
sidebars.pop();
return sidebars;
}
function compareElements(original, displayed) {
let attrs = ["label", "key", "disabled", "hidden", "origin", "image", "checked"];
for (let attr of attrs) {
is(displayed.getAttribute(attr), original.getAttribute(attr), "Should have the same " + attr + " attribute");
}
}
function compareList(original, displayed) {
is(displayed.length, original.length, "Should have the same number of children");
for (let i = 0; i < Math.min(original.length, displayed.length); i++) {
compareElements(displayed[i], original[i]);
}
}
let showSidebarPopup = Task.async(function*() {
let button = document.getElementById("sidebar-button");
let subview = document.getElementById("PanelUI-sidebar");
let subviewShownPromise = subviewShown(subview);
EventUtils.synthesizeMouseAtCenter(button, {});
yield subviewShownPromise;
return waitForCondition(() => !subview.panelMultiView.hasAttribute("transitioning"));
});
// Check the sidebar widget shows the default items
add_task(function*() {
CustomizableUI.addWidgetToArea("sidebar-button", "nav-bar");
yield showSidebarPopup();
let sidebars = getSidebarList();
let displayed = [...document.getElementById("PanelUI-sidebarItems").children];
compareList(sidebars, displayed);
let subview = document.getElementById("PanelUI-sidebar");
let subviewHiddenPromise = subviewHidden(subview);
document.getElementById("customizationui-widget-panel").hidePopup();
yield subviewHiddenPromise;
yield resetCustomization();
});
function add_sidebar_task(description, setup, teardown) {
add_task(function*() {
info(description);
createSidebarItem();
yield setup();
CustomizableUI.addWidgetToArea("sidebar-button", "nav-bar");
yield showSidebarPopup();
let sidebars = getSidebarList();
let displayed = [...document.getElementById("PanelUI-sidebarItems").children];
compareList(sidebars, displayed);
is(displayed[0].label, "Test Sidebar", "Should have the right element at the top");
let subview = document.getElementById("PanelUI-sidebar");
let subviewHiddenPromise = subviewHidden(subview);
EventUtils.synthesizeMouseAtCenter(displayed[0], {});
yield subviewHiddenPromise;
yield teardown();
gTestSidebarItem.remove();
return resetCustomization();
});
}
add_sidebar_task(
"Check that a sidebar that uses a command event listener works",
function*() {
gTestSidebarItem.addEventListener("command", sawEvent);
}, function*() {
checkExpectedEvents({ command: 1 });
});
add_sidebar_task(
"Check that a sidebar that uses a click event listener works",
function*() {
gTestSidebarItem.addEventListener("click", sawEvent);
}, function*() {
checkExpectedEvents({ click: 1 });
});
add_sidebar_task(
"Check that a sidebar that uses both click and command event listeners works",
function*() {
gTestSidebarItem.addEventListener("command", sawEvent);
gTestSidebarItem.addEventListener("click", sawEvent);
}, function*() {
checkExpectedEvents({ command: 1, click: 1 });
});
add_sidebar_task(
"Check that a sidebar that uses an oncommand attribute works",
function*() {
gTestSidebarItem.setAttribute("oncommand", "sawEvent(event, true)");
}, function*() {
checkExpectedEvents({ oncommand: 1 });
});
add_sidebar_task(
"Check that a sidebar that uses an onclick attribute works",
function*() {
gTestSidebarItem.setAttribute("onclick", "sawEvent(event, true)");
}, function*() {
checkExpectedEvents({ onclick: 1 });
});
add_sidebar_task(
"Check that a sidebar that uses both onclick and oncommand attributes works",
function*() {
gTestSidebarItem.setAttribute("onclick", "sawEvent(event, true)");
gTestSidebarItem.setAttribute("oncommand", "sawEvent(event, true)");
}, function*() {
checkExpectedEvents({ onclick: 1, oncommand: 1 });
});
add_sidebar_task(
"Check that a sidebar that uses an onclick attribute and a command listener works",
function*() {
gTestSidebarItem.setAttribute("onclick", "sawEvent(event, true)");
gTestSidebarItem.addEventListener("command", sawEvent);
}, function*() {
checkExpectedEvents({ onclick: 1, command: 1 });
});
add_sidebar_task(
"Check that a sidebar that uses an oncommand attribute and a click listener works",
function*() {
gTestSidebarItem.setAttribute("oncommand", "sawEvent(event, true)");
gTestSidebarItem.addEventListener("click", sawEvent);
}, function*() {
checkExpectedEvents({ click: 1, oncommand: 1 });
});
add_sidebar_task(
"A sidebar with both onclick attribute and click listener sees only one event :(",
function*() {
gTestSidebarItem.setAttribute("onclick", "sawEvent(event, true)");
gTestSidebarItem.addEventListener("click", sawEvent);
}, function*() {
checkExpectedEvents({ onclick: 1 });
});
add_sidebar_task(
"A sidebar with both oncommand attribute and command listener sees only one event :(",
function*() {
gTestSidebarItem.setAttribute("oncommand", "sawEvent(event, true)");
gTestSidebarItem.addEventListener("command", sawEvent);
}, function*() {
checkExpectedEvents({ oncommand: 1 });
});
add_sidebar_task(
"Check that a sidebar that uses a broadcaster with an oncommand attribute works",
function*() {
let broadcaster = document.createElement("broadcaster");
broadcaster.setAttribute("id", "testbroadcaster");
broadcaster.setAttribute("oncommand", "sawEvent(event, true)");
broadcaster.setAttribute("label", "Test Sidebar");
document.getElementById("mainBroadcasterSet").appendChild(broadcaster);
gTestSidebarItem.setAttribute("observes", "testbroadcaster");
}, function*() {
checkExpectedEvents({ oncommand: 1 });
document.getElementById("testbroadcaster").remove();
});
add_sidebar_task(
"Check that a sidebar that uses a broadcaster with an onclick attribute works",
function*() {
let broadcaster = document.createElement("broadcaster");
broadcaster.setAttribute("id", "testbroadcaster");
broadcaster.setAttribute("onclick", "sawEvent(event, true)");
broadcaster.setAttribute("label", "Test Sidebar");
document.getElementById("mainBroadcasterSet").appendChild(broadcaster);
gTestSidebarItem.setAttribute("observes", "testbroadcaster");
}, function*() {
checkExpectedEvents({ onclick: 1 });
document.getElementById("testbroadcaster").remove();
});
add_sidebar_task(
"Check that a sidebar that uses a broadcaster with both onclick and oncommand attributes works",
function*() {
let broadcaster = document.createElement("broadcaster");
broadcaster.setAttribute("id", "testbroadcaster");
broadcaster.setAttribute("onclick", "sawEvent(event, true)");
broadcaster.setAttribute("oncommand", "sawEvent(event, true)");
broadcaster.setAttribute("label", "Test Sidebar");
document.getElementById("mainBroadcasterSet").appendChild(broadcaster);
gTestSidebarItem.setAttribute("observes", "testbroadcaster");
}, function*() {
checkExpectedEvents({ onclick: 1, oncommand: 1 });
document.getElementById("testbroadcaster").remove();
});
add_sidebar_task(
"Check that a sidebar with a click listener and a broadcaster with an oncommand attribute works",
function*() {
let broadcaster = document.createElement("broadcaster");
broadcaster.setAttribute("id", "testbroadcaster");
broadcaster.setAttribute("oncommand", "sawEvent(event, true)");
broadcaster.setAttribute("label", "Test Sidebar");
document.getElementById("mainBroadcasterSet").appendChild(broadcaster);
gTestSidebarItem.setAttribute("observes", "testbroadcaster");
gTestSidebarItem.addEventListener("click", sawEvent);
}, function*() {
checkExpectedEvents({ click: 1, oncommand: 1 });
document.getElementById("testbroadcaster").remove();
});
add_sidebar_task(
"Check that a sidebar with a command listener and a broadcaster with an onclick attribute works",
function*() {
let broadcaster = document.createElement("broadcaster");
broadcaster.setAttribute("id", "testbroadcaster");
broadcaster.setAttribute("onclick", "sawEvent(event, true)");
broadcaster.setAttribute("label", "Test Sidebar");
document.getElementById("mainBroadcasterSet").appendChild(broadcaster);
gTestSidebarItem.setAttribute("observes", "testbroadcaster");
gTestSidebarItem.addEventListener("command", sawEvent);
}, function*() {
checkExpectedEvents({ onclick: 1, command: 1 });
document.getElementById("testbroadcaster").remove();
});
add_sidebar_task(
"Check that a sidebar with a click listener and a broadcaster with an onclick " +
"attribute only sees one event :(",
function*() {
let broadcaster = document.createElement("broadcaster");
broadcaster.setAttribute("id", "testbroadcaster");
broadcaster.setAttribute("onclick", "sawEvent(event, true)");
broadcaster.setAttribute("label", "Test Sidebar");
document.getElementById("mainBroadcasterSet").appendChild(broadcaster);
gTestSidebarItem.setAttribute("observes", "testbroadcaster");
gTestSidebarItem.addEventListener("click", sawEvent);
}, function*() {
checkExpectedEvents({ onclick: 1 });
document.getElementById("testbroadcaster").remove();
});
add_sidebar_task(
"Check that a sidebar with a command listener and a broadcaster with an oncommand " +
"attribute only sees one event :(",
function*() {
let broadcaster = document.createElement("broadcaster");
broadcaster.setAttribute("id", "testbroadcaster");
broadcaster.setAttribute("oncommand", "sawEvent(event, true)");
broadcaster.setAttribute("label", "Test Sidebar");
document.getElementById("mainBroadcasterSet").appendChild(broadcaster);
gTestSidebarItem.setAttribute("observes", "testbroadcaster");
gTestSidebarItem.addEventListener("command", sawEvent);
}, function*() {
checkExpectedEvents({ oncommand: 1 });
document.getElementById("testbroadcaster").remove();
});
add_sidebar_task(
"Check that a sidebar that uses a command element with a command event listener works",
function*() {
let command = document.createElement("command");
command.setAttribute("id", "testcommand");
document.getElementById("mainCommandSet").appendChild(command);
command.addEventListener("command", sawEvent);
gTestSidebarItem.setAttribute("command", "testcommand");
}, function*() {
checkExpectedEvents({ command: 1 });
document.getElementById("testcommand").remove();
});
add_sidebar_task(
"Check that a sidebar that uses a command element with an oncommand attribute works",
function*() {
let command = document.createElement("command");
command.setAttribute("id", "testcommand");
command.setAttribute("oncommand", "sawEvent(event, true)");
document.getElementById("mainCommandSet").appendChild(command);
gTestSidebarItem.setAttribute("command", "testcommand");
}, function*() {
checkExpectedEvents({ oncommand: 1 });
document.getElementById("testcommand").remove();
});
add_sidebar_task("Check that a sidebar that uses a command element with a " +
"command event listener and oncommand attribute works",
function*() {
let command = document.createElement("command");
command.setAttribute("id", "testcommand");
command.setAttribute("oncommand", "sawEvent(event, true)");
document.getElementById("mainCommandSet").appendChild(command);
command.addEventListener("command", sawEvent);
gTestSidebarItem.setAttribute("command", "testcommand");
}, function*() {
checkExpectedEvents({ command: 1, oncommand: 1 });
document.getElementById("testcommand").remove();
});
add_sidebar_task(
"A sidebar with a command element will still see click events",
function*() {
let command = document.createElement("command");
command.setAttribute("id", "testcommand");
command.setAttribute("oncommand", "sawEvent(event, true)");
document.getElementById("mainCommandSet").appendChild(command);
command.addEventListener("command", sawEvent);
gTestSidebarItem.setAttribute("command", "testcommand");
gTestSidebarItem.addEventListener("click", sawEvent);
}, function*() {
checkExpectedEvents({ click: 1, command: 1, oncommand: 1 });
document.getElementById("testcommand").remove();
});

View File

@ -59,6 +59,9 @@
title="&prefWindow.titleGNOME;">
#endif
<html:link rel="shortcut icon"
href="chrome://browser/skin/preferences/in-content/favicon.ico"/>
<script type="application/javascript"
src="chrome://browser/content/utilityOverlay.js"/>
<script type="application/javascript"

View File

@ -21,7 +21,7 @@
<content>
<xul:hbox class="notification-inner outset" flex="1" xbl:inherits="type">
<xul:hbox anonid="details" align="center" flex="1">
<xul:image anonid="messageImage" class="messageImage" xbl:inherits="src=image,type,value"/>
<xul:image anonid="messageImage" class="messageImage"/>
<xul:deck anonid="translationStates" selectedIndex="0">
<!-- offer to translate -->
@ -107,6 +107,15 @@
if (activeElt && deck.contains(activeElt))
activeElt.blur();
let stateName;
for (let name of ["OFFER", "TRANSLATING", "TRANSLATED", "ERROR"]) {
if (this.translation["STATE_" + name] == val) {
stateName = name.toLowerCase();
break;
}
}
this.setAttribute("state", stateName);
deck.selectedIndex = val;
]]>
</setter>

View File

@ -1223,6 +1223,7 @@ Toolbox.prototype = {
if (this.target.isLocalTab) {
this._requisition.destroy();
}
this._telemetry.toolClosed("toolbox");
this._telemetry.destroy();
return this._destroyer = promise.all(outstanding).then(() => {

View File

@ -61,6 +61,8 @@ let {XPCOMUtils} = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
Telemetry.prototype = {
_histograms: {
toolbox: {
histogram: "DEVTOOLS_TOOLBOX_OPENED_BOOLEAN",
userHistogram: "DEVTOOLS_TOOLBOX_OPENED_PER_USER_FLAG",
timerHistogram: "DEVTOOLS_TOOLBOX_TIME_ACTIVE_SECONDS"
},
options: {
@ -212,8 +214,6 @@ Telemetry.prototype = {
*/
log: function(histogramId, value) {
if (histogramId) {
let histogram;
try {
let histogram = Services.telemetry.getHistogramById(histogramId);
histogram.add(value);

View File

@ -20,6 +20,7 @@ support-files =
[browser_telemetry_button_scratchpad.js]
[browser_telemetry_button_tilt.js]
[browser_telemetry_sidebar.js]
[browser_telemetry_toolbox.js]
[browser_telemetry_toolboxtabs_inspector.js]
[browser_telemetry_toolboxtabs_jsdebugger.js]
[browser_telemetry_toolboxtabs_jsprofiler.js]

View File

@ -73,6 +73,8 @@ function checkResults() {
if (histId.endsWith("OPENED_PER_USER_FLAG")) {
ok(value.length === 1 && value[0] === true,
"Per user value " + histId + " has a single value of true");
} else if (histId === "DEVTOOLS_TOOLBOX_OPENED_BOOLEAN") {
is(value.length, 1, histId + " has only one entry");
} else if (histId.endsWith("OPENED_BOOLEAN")) {
ok(value.length > 1, histId + " has more than one entry");

View File

@ -0,0 +1,103 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const TEST_URI = "data:text/html;charset=utf-8,<p>browser_telemetry_toolbox.js</p>";
// Because we need to gather stats for the period of time that a tool has been
// opened we make use of setTimeout() to create tool active times.
const TOOL_DELAY = 200;
let {Promise: promise} = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {});
let {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
let require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require;
let Telemetry = require("devtools/shared/telemetry");
function init() {
Telemetry.prototype.telemetryInfo = {};
Telemetry.prototype._oldlog = Telemetry.prototype.log;
Telemetry.prototype.log = function(histogramId, value) {
if (histogramId) {
if (!this.telemetryInfo[histogramId]) {
this.telemetryInfo[histogramId] = [];
}
this.telemetryInfo[histogramId].push(value);
}
};
openToolboxThreeTimes();
}
let pass = 0;
function openToolboxThreeTimes() {
let target = TargetFactory.forTab(gBrowser.selectedTab);
gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
info("Toolbox opened");
toolbox.once("destroyed", function() {
if (pass++ === 3) {
checkResults();
} else {
openToolboxThreeTimes();
}
});
// We use a timeout to check the toolbox's active time
setTimeout(function() {
gDevTools.closeToolbox(target);
}, TOOL_DELAY);
}).then(null, console.error);
}
function checkResults() {
let result = Telemetry.prototype.telemetryInfo;
for (let [histId, value] of Iterator(result)) {
if (histId.endsWith("OPENED_PER_USER_FLAG")) {
ok(value.length === 1 && value[0] === true,
"Per user value " + histId + " has a single value of true");
} else if (histId.endsWith("OPENED_BOOLEAN")) {
ok(value.length > 1, histId + " has more than one entry");
let okay = value.every(function(element) {
return element === true;
});
ok(okay, "All " + histId + " entries are === true");
} else if (histId.endsWith("TIME_ACTIVE_SECONDS")) {
ok(value.length > 1, histId + " has more than one entry");
let okay = value.every(function(element) {
return element > 0;
});
ok(okay, "All " + histId + " entries have time > 0");
}
}
finishUp();
}
function finishUp() {
gBrowser.removeCurrentTab();
Telemetry.prototype.log = Telemetry.prototype._oldlog;
delete Telemetry.prototype._oldlog;
delete Telemetry.prototype.telemetryInfo;
TargetFactory = Services = promise = require = null;
finish();
}
function test() {
waitForExplicitFinish();
gBrowser.selectedTab = gBrowser.addTab();
gBrowser.selectedBrowser.addEventListener("load", function() {
gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
waitForFocus(init, content);
}, true);
content.location = TEST_URI;
}

View File

@ -135,6 +135,7 @@ 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/favicon.ico (../shared/incontentprefs/favicon.ico)
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)
@ -180,6 +181,7 @@ browser.jar:
skin/classic/browser/tabview/stack-expander.png (tabview/stack-expander.png)
skin/classic/browser/tabview/tabview.png (tabview/tabview.png)
skin/classic/browser/tabview/tabview.css (tabview/tabview.css)
skin/classic/browser/translating-16.png (../shared/translation/translating-16.png)
skin/classic/browser/translation-16.png (../shared/translation/translation-16.png)
* skin/classic/browser/devtools/common.css (../shared/devtools/common.css)
* skin/classic/browser/devtools/dark-theme.css (../shared/devtools/dark-theme.css)

View File

@ -225,6 +225,7 @@ 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/favicon.ico (../shared/incontentprefs/favicon.ico)
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)
@ -297,6 +298,8 @@ browser.jar:
skin/classic/browser/tabview/stack-expander.png (tabview/stack-expander.png)
skin/classic/browser/tabview/tabview.png (tabview/tabview.png)
skin/classic/browser/tabview/tabview.css (tabview/tabview.css)
skin/classic/browser/translating-16.png (../shared/translation/translating-16.png)
skin/classic/browser/translating-16@2x.png (../shared/translation/translating-16@2x.png)
skin/classic/browser/translation-16.png (../shared/translation/translation-16.png)
skin/classic/browser/translation-16@2x.png (../shared/translation/translation-16@2x.png)
* skin/classic/browser/devtools/common.css (../shared/devtools/common.css)

Binary file not shown.

After

Width:  |  Height:  |  Size: 933 B

View File

@ -15,6 +15,17 @@ notification[value="translation"] .messageImage {
}
}
notification[value="translation"][state="translating"] .messageImage {
list-style-image: url(chrome://browser/skin/translating-16.png);
-moz-image-region: auto;
}
@media (min-resolution: 1.25dppx) {
notification[value="translation"][state="translating"] .messageImage {
list-style-image: url(chrome://browser/skin/translating-16@2x.png);
}
}
notification[value="translation"] {
min-height: 40px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -160,6 +160,7 @@ 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/favicon.ico (../shared/incontentprefs/favicon.ico)
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)
@ -217,6 +218,7 @@ browser.jar:
skin/classic/browser/tabview/tabview.png (tabview/tabview.png)
skin/classic/browser/tabview/tabview-inverted.png (tabview/tabview-inverted.png)
skin/classic/browser/tabview/tabview.css (tabview/tabview.css)
skin/classic/browser/translating-16.png (../shared/translation/translating-16.png)
skin/classic/browser/translation-16.png (../shared/translation/translation-16.png)
* skin/classic/browser/devtools/common.css (../shared/devtools/common.css)
* skin/classic/browser/devtools/dark-theme.css (../shared/devtools/dark-theme.css)
@ -551,6 +553,7 @@ 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/favicon.ico (../shared/incontentprefs/favicon.ico)
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)
@ -607,6 +610,7 @@ browser.jar:
skin/classic/aero/browser/tabview/tabview.png (tabview/tabview.png)
skin/classic/aero/browser/tabview/tabview-inverted.png (tabview/tabview-inverted.png)
skin/classic/aero/browser/tabview/tabview.css (tabview/tabview.css)
skin/classic/aero/browser/translating-16.png (../shared/translation/translating-16.png)
skin/classic/aero/browser/translation-16.png (../shared/translation/translation-16.png)
* skin/classic/aero/browser/devtools/common.css (../shared/devtools/common.css)
* skin/classic/aero/browser/devtools/dark-theme.css (../shared/devtools/dark-theme.css)

View File

@ -15,18 +15,16 @@ import java.util.Vector;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.AndroidGamepadManager;
import org.mozilla.gecko.DynamicToolbar.PinReason;
import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
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.db.BrowserContract.Combined;
import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.SuggestedSites;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.favicons.Favicons;
import org.mozilla.gecko.favicons.LoadFaviconTask;
import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
@ -43,9 +41,9 @@ import org.mozilla.gecko.health.HealthRecorder;
import org.mozilla.gecko.health.SessionInformation;
import org.mozilla.gecko.home.BrowserSearch;
import org.mozilla.gecko.home.HomeBanner;
import org.mozilla.gecko.home.HomePanelsManager;
import org.mozilla.gecko.home.HomePager;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import org.mozilla.gecko.home.HomePanelsManager;
import org.mozilla.gecko.home.SearchEngine;
import org.mozilla.gecko.menu.GeckoMenu;
import org.mozilla.gecko.menu.GeckoMenuItem;
@ -108,8 +106,8 @@ import android.view.ViewStub;
import android.view.ViewTreeObserver;
import android.view.Window;
import android.view.animation.Interpolator;
import android.widget.RelativeLayout;
import android.widget.ListView;
import android.widget.RelativeLayout;
import android.widget.Toast;
import android.widget.ViewFlipper;
@ -1306,8 +1304,10 @@ abstract public class BrowserApp extends GeckoApp
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
if (menu != null)
if (menu != null) {
menu.findItem(R.id.settings).setEnabled(true);
menu.findItem(R.id.help).setEnabled(true);
}
}
});
@ -2282,8 +2282,10 @@ abstract public class BrowserApp extends GeckoApp
if (aMenu == null)
return false;
if (!GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning))
if (!GeckoThread.checkLaunchState(GeckoThread.LaunchState.GeckoRunning)) {
aMenu.findItem(R.id.settings).setEnabled(false);
aMenu.findItem(R.id.help).setEnabled(false);
}
Tab tab = Tabs.getInstance().getSelectedTab();
MenuItem bookmark = aMenu.findItem(R.id.bookmark);
@ -2496,6 +2498,16 @@ abstract public class BrowserApp extends GeckoApp
return true;
}
if (itemId == R.id.help) {
final String VERSION = AppConstants.MOZ_APP_VERSION;
final String OS = AppConstants.OS_TARGET;
final String LOCALE = BrowserLocaleManager.getLanguageTag(Locale.getDefault());
final String URL = getResources().getString(R.string.help_link, VERSION, OS, LOCALE);
Tabs.getInstance().loadUrlInTab(URL);
return true;
}
if (itemId == R.id.addons) {
Tabs.getInstance().loadUrlInTab(AboutPages.ADDONS);
return true;

View File

@ -5,15 +5,6 @@
package org.mozilla.gecko;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.util.Log;
import java.io.File;
import java.util.Collection;
import java.util.HashSet;
@ -27,6 +18,15 @@ import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.util.GeckoJarReader;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.util.Log;
/**
* This class manages persistence, application, and otherwise handling of
* user-specified locales.
@ -132,6 +132,8 @@ public class BrowserLocaleManager implements LocaleManager {
receiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
final Locale current = systemLocale;
// We don't trust Locale.getDefault() here, because we make a
// habit of mutating it! Use the one Android supplies, because
// that gets regularly reset.
@ -139,6 +141,8 @@ public class BrowserLocaleManager implements LocaleManager {
// yet swizzled Locale during static initialization.
systemLocale = context.getResources().getConfiguration().locale;
systemLocaleDidChange = true;
Log.d(LOG_TAG, "System locale changed from " + current + " to " + systemLocale);
}
};
context.registerReceiver(receiver, new IntentFilter(Intent.ACTION_LOCALE_CHANGED));
@ -157,6 +161,7 @@ public class BrowserLocaleManager implements LocaleManager {
public void correctLocale(Context context, Resources res, Configuration config) {
final Locale current = getCurrentLocale(context);
if (current == null) {
Log.d(LOG_TAG, "No selected locale. No correction needed.");
return;
}
@ -180,6 +185,48 @@ public class BrowserLocaleManager implements LocaleManager {
res.updateConfiguration(config, null);
}
/**
* We can be in one of two states.
*
* If the user has not explicitly chosen a Firefox-specific locale, we say
* we are "mirroring" the system locale.
*
* When we are not mirroring, system locale changes do not impact Firefox
* and are essentially ignored; the user's locale selection is the only
* thing we care about, and we actively correct incoming configuration
* changes to reflect the user's chosen locale.
*
* By contrast, when we are mirroring, system locale changes cause Firefox
* to reflect the new system locale, as if the user picked the new locale.
*
* If we're currently mirroring the system locale, this method returns the
* supplied configuration's locale, unless the current activity locale is
* correct. , If we're not currently mirroring, this methodupdates the
* configuration object to match the user's currently selected locale, and
* returns that, unless the current activity locale is correct.
*
* If the current activity locale is correct, returns null.
*
* The caller is expected to redisplay themselves accordingly.
*
* This method is intended to be called from inside
* <code>onConfigurationChanged(Configuration)</code> as part of a strategy
* to detect and either apply or undo system locale changes.
*/
@Override
public Locale onSystemConfigurationChanged(final Context context, final Resources resources, final Configuration configuration, final Locale currentActivityLocale) {
if (!isMirroringSystemLocale(context)) {
correctLocale(context, resources, configuration);
}
final Locale changed = configuration.locale;
if (changed.equals(currentActivityLocale)) {
return null;
}
return changed;
}
@Override
public String getAndApplyPersistedLocale(Context context) {
initialize(context);
@ -323,6 +370,10 @@ public class BrowserLocaleManager implements LocaleManager {
return locale.toString();
}
private boolean isMirroringSystemLocale(final Context context) {
return getPersistedLocale(context) == null;
}
/**
* Examines <code>multilocale.json</code>, returning the included list of
* locale codes.

View File

@ -16,7 +16,6 @@ import java.net.URL;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
@ -209,6 +208,7 @@ public abstract class GeckoApp
private String mPrivateBrowsingSession;
private volatile HealthRecorder mHealthRecorder = null;
private volatile Locale mLastLocale = null;
private int mSignalStrenth;
private PhoneStateListener mPhoneStateListener = null;
@ -1277,9 +1277,10 @@ public abstract class GeckoApp
public void run() {
final SharedPreferences prefs = GeckoApp.this.getSharedPreferences();
// Wait until now to set this, because we'd rather throw an exception than
// Wait until now to set this, because we'd rather throw an exception than
// have a caller of BrowserLocaleManager regress startup.
BrowserLocaleManager.getInstance().initialize(getApplicationContext());
final LocaleManager localeManager = BrowserLocaleManager.getInstance();
localeManager.initialize(getApplicationContext());
SessionInformation previousSession = SessionInformation.fromSharedPrefs(prefs);
if (previousSession.wasKilled()) {
@ -1303,7 +1304,7 @@ public abstract class GeckoApp
Log.i(LOGTAG, "Creating HealthRecorder.");
final String osLocale = Locale.getDefault().toString();
String appLocale = BrowserLocaleManager.getInstance().getAndApplyPersistedLocale(GeckoApp.this);
String appLocale = localeManager.getAndApplyPersistedLocale(GeckoApp.this);
Log.d(LOGTAG, "OS locale is " + osLocale + ", app locale is " + appLocale);
if (appLocale == null) {
@ -1351,6 +1352,11 @@ public abstract class GeckoApp
throw new RuntimeException("onLocaleReady must always be called from the UI thread.");
}
final Locale loc = BrowserLocaleManager.parseLocaleCode(locale);
if (loc.equals(mLastLocale)) {
Log.d(LOGTAG, "New locale same as old; onLocaleReady has nothing to do.");
}
// The URL bar hint needs to be populated.
TextView urlBar = (TextView) findViewById(R.id.url_bar_title);
if (urlBar != null) {
@ -1360,8 +1366,13 @@ public abstract class GeckoApp
Log.d(LOGTAG, "No URL bar in GeckoApp. Not loading localized hint string.");
}
mLastLocale = loc;
// Allow onConfigurationChanged to take care of the rest.
onConfigurationChanged(getResources().getConfiguration());
// We don't call this.onConfigurationChanged, because (a) that does
// work that's unnecessary after this locale action, and (b) it can
// cause a loop! See Bug 1011008, Comment 12.
super.onConfigurationChanged(getResources().getConfiguration());
}
protected void initializeChrome() {
@ -2155,7 +2166,12 @@ public abstract class GeckoApp
@Override
public void onConfigurationChanged(Configuration newConfig) {
Log.d(LOGTAG, "onConfigurationChanged: " + newConfig.locale);
BrowserLocaleManager.getInstance().correctLocale(this, getResources(), newConfig);
final LocaleManager localeManager = BrowserLocaleManager.getInstance();
final Locale changed = localeManager.onSystemConfigurationChanged(this, getResources(), newConfig, mLastLocale);
if (changed != null) {
onLocaleChanged(BrowserLocaleManager.getLanguageTag(changed));
}
// onConfigurationChanged is not called for 180 degree orientation changes,
// we will miss such rotations and the screen orientation will not be

View File

@ -10,6 +10,12 @@ import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
/**
* Implement this interface to provide Fennec's locale switching functionality.
*
* The LocaleManager is responsible for persisting and applying selected locales,
* and correcting configurations after Android has changed them.
*/
public interface LocaleManager {
void initialize(Context context);
Locale getCurrentLocale(Context context);
@ -19,4 +25,12 @@ public interface LocaleManager {
String setSelectedLocale(Context context, String localeCode);
boolean systemLocaleDidChange();
void resetToSystemLocale(Context context);
/**
* Call this in your onConfigurationChanged handler. This method is expected
* to do the appropriate thing: if the user has selected a locale, it
* corrects the incoming configuration; if not, it signals the new locale to
* use.
*/
Locale onSystemConfigurationChanged(Context context, Resources resources, Configuration configuration, Locale currentActivityLocale);
}

View File

@ -557,7 +557,6 @@ sync_java_files = [
'fxa/activities/FxAccountCreateAccountActivity.java',
'fxa/activities/FxAccountCreateAccountNotAllowedActivity.java',
'fxa/activities/FxAccountGetStartedActivity.java',
'fxa/activities/FxAccountSetupTask.java',
'fxa/activities/FxAccountSignInActivity.java',
'fxa/activities/FxAccountStatusActivity.java',
'fxa/activities/FxAccountStatusFragment.java',
@ -591,6 +590,10 @@ sync_java_files = [
'fxa/sync/FxAccountSyncService.java',
'fxa/sync/FxAccountSyncStatusHelper.java',
'fxa/sync/SchedulePolicy.java',
'fxa/tasks/FxAccountCodeResender.java',
'fxa/tasks/FxAccountCreateAccountTask.java',
'fxa/tasks/FxAccountSetupTask.java',
'fxa/tasks/FxAccountSignInTask.java',
'sync/AlreadySyncingException.java',
'sync/BackoffHandler.java',
'sync/BadRequiredFieldJSONException.java',

View File

@ -6,7 +6,6 @@ package org.mozilla.gecko.background.healthreport;
import org.mozilla.gecko.background.healthreport.Environment.UIType;
import org.mozilla.gecko.background.healthreport.EnvironmentBuilder.ConfigurationProvider;
import org.mozilla.gecko.sync.jpake.stage.GetRequestStage.GetStepTimerTask;
import org.mozilla.gecko.util.HardwareUtils;
import android.content.Context;

View File

@ -18,7 +18,6 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.Distribution;
import org.mozilla.gecko.R;
import org.mozilla.gecko.db.BrowserContract.Bookmarks;
import org.mozilla.gecko.db.BrowserContract.Combined;
@ -28,6 +27,7 @@ import org.mozilla.gecko.db.BrowserContract.History;
import org.mozilla.gecko.db.BrowserContract.Obsolete;
import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
import org.mozilla.gecko.db.BrowserContract.Thumbnails;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.gfx.BitmapUtils;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.util.GeckoJarReader;

View File

@ -3,21 +3,10 @@
* 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 org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
package org.mozilla.gecko.distribution;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@ -27,10 +16,30 @@ import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Queue;
import java.util.Scanner;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoEvent;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.mozglue.RobocopTarget;
import org.mozilla.gecko.util.ThreadUtils;
import android.app.Activity;
import android.content.Context;
import android.content.SharedPreferences;
import android.util.Log;
/**
* Handles distribution file loading and fetching,
* and the corresponding hand-offs to Gecko.
*/
public final class Distribution {
private static final String LOGTAG = "GeckoDistribution";
@ -38,6 +47,29 @@ public final class Distribution {
private static final int STATE_NONE = 1;
private static final int STATE_SET = 2;
private static Distribution instance;
private final Context context;
private final String packagePath;
private final String prefsBranch;
private volatile int state = STATE_UNKNOWN;
private File distributionDir = null;
private final Queue<Runnable> onDistributionReady = new ConcurrentLinkedQueue<Runnable>();
/**
* This is a little bit of a bad singleton, because in principle a Distribution
* can be created with arbitrary paths. So we only have one path to get here, and
* it uses the default arguments. Watch out if you're creating your own instances!
*/
public static synchronized Distribution getInstance(Context context) {
if (instance == null) {
instance = new Distribution(context);
}
return instance;
}
public static class DistributionDescriptor {
public final boolean valid;
public final String id;
@ -80,20 +112,12 @@ public final class Distribution {
}
}
/**
* Initializes distribution if it hasn't already been initalized. Sends
* messages to Gecko as appropriate.
*
* @param packagePath where to look for the distribution directory.
*/
@RobocopTarget
public static void init(final Context context, final String packagePath, final String prefsPath) {
private static void init(final Distribution distribution) {
// Read/write preferences and files on the background thread.
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
Distribution dist = new Distribution(context, packagePath, prefsPath);
boolean distributionSet = dist.doInit();
boolean distributionSet = distribution.doInit();
if (distributionSet) {
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("Distribution:Set", ""));
}
@ -101,12 +125,23 @@ public final class Distribution {
});
}
/**
* Initializes distribution if it hasn't already been initialized. Sends
* messages to Gecko as appropriate.
*
* @param packagePath where to look for the distribution directory.
*/
@RobocopTarget
public static void init(final Context context, final String packagePath, final String prefsPath) {
init(new Distribution(context, packagePath, prefsPath));
}
/**
* Use <code>Context.getPackageResourcePath</code> to find an implicit
* package path.
* package path. Reuses the existing Distribution if one exists.
*/
public static void init(final Context context) {
Distribution.init(context, context.getPackageResourcePath(), null);
Distribution.init(Distribution.getInstance(context));
}
/**
@ -118,13 +153,6 @@ public final class Distribution {
return dist.getBookmarks();
}
private final Context context;
private final String packagePath;
private final String prefsBranch;
private int state = STATE_UNKNOWN;
private File distributionDir = null;
/**
* @param packagePath where to look for the distribution directory.
*/
@ -138,142 +166,6 @@ public final class Distribution {
this(context, context.getPackageResourcePath(), null);
}
/**
* Don't call from the main thread.
*
* @return true if we've set a distribution.
*/
private boolean doInit() {
// Bail if we've already tried to initialize the distribution, and
// there wasn't one.
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) {
return false;
}
// We've done the work once; don't do it again.
if (this.state == STATE_SET) {
// Note that we don't compute the distribution directory.
// Call `ensureDistributionDir` if you need it.
return true;
}
boolean distributionSet = false;
try {
// First, try copying distribution files out of the APK.
distributionSet = copyFiles();
if (distributionSet) {
// We always copy to the data dir, and we only copy files from
// a 'distribution' subdirectory. Track our dist dir now that
// we know it.
this.distributionDir = new File(getDataDir(), "distribution/");
}
} catch (IOException e) {
Log.e(LOGTAG, "Error copying distribution files", e);
}
if (!distributionSet) {
// If there aren't any distribution files in the APK, look in the /system directory.
File distDir = getSystemDistributionDir();
if (distDir.exists()) {
distributionSet = true;
this.distributionDir = distDir;
}
}
this.state = distributionSet ? STATE_SET : STATE_NONE;
settings.edit().putInt(keyName, this.state).commit();
return distributionSet;
}
/**
* Copies the /distribution folder out of the APK and into the app's data directory.
* Returns true if distribution files were found and copied.
*/
private boolean copyFiles() throws IOException {
File applicationPackage = new File(packagePath);
ZipFile zip = new ZipFile(applicationPackage);
boolean distributionSet = false;
Enumeration<? extends ZipEntry> zipEntries = zip.entries();
byte[] buffer = new byte[1024];
while (zipEntries.hasMoreElements()) {
ZipEntry fileEntry = zipEntries.nextElement();
String name = fileEntry.getName();
if (!name.startsWith("distribution/")) {
continue;
}
distributionSet = true;
File outFile = new File(getDataDir(), name);
File dir = outFile.getParentFile();
if (!dir.exists()) {
if (!dir.mkdirs()) {
Log.e(LOGTAG, "Unable to create directories: " + dir.getAbsolutePath());
continue;
}
}
InputStream fileStream = zip.getInputStream(fileEntry);
OutputStream outStream = new FileOutputStream(outFile);
int count;
while ((count = fileStream.read(buffer)) != -1) {
outStream.write(buffer, 0, count);
}
fileStream.close();
outStream.close();
outFile.setLastModified(fileEntry.getTime());
}
zip.close();
return distributionSet;
}
/**
* After calling this method, either <code>distributionDir</code>
* will be set, or there is no distribution in use.
*
* Only call after init.
*/
private File ensureDistributionDir() {
if (this.distributionDir != null) {
return this.distributionDir;
}
if (this.state != STATE_SET) {
return null;
}
// After init, we know that either we've copied a distribution out of
// the APK, or it exists in /system/.
// Look in each location in turn.
// (This could be optimized by caching the path in shared prefs.)
File copied = new File(getDataDir(), "distribution/");
if (copied.exists()) {
return this.distributionDir = copied;
}
File system = getSystemDistributionDir();
if (system.exists()) {
return this.distributionDir = system;
}
return null;
}
/**
* Helper to grab a file in the distribution directory.
*
@ -281,7 +173,8 @@ public final class Distribution {
* doesn't exist. Ensures init first.
*/
public File getDistributionFile(String name) {
Log.i(LOGTAG, "Getting file from distribution.");
Log.d(LOGTAG, "Getting file from distribution.");
if (this.state == STATE_UNKNOWN) {
if (!this.doInit()) {
return null;
@ -346,6 +239,210 @@ public final class Distribution {
return null;
}
/**
* Don't call from the main thread.
*
* Postcondition: if this returns true, distributionDir will have been
* set and populated.
*
* @return true if we've set a distribution.
*/
private boolean doInit() {
ThreadUtils.assertNotOnUiThread();
// Bail if we've already tried to initialize the distribution, and
// there wasn't one.
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) {
runReadyQueue();
return false;
}
// We've done the work once; don't do it again.
if (this.state == STATE_SET) {
// Note that we don't compute the distribution directory.
// Call `ensureDistributionDir` if you need it.
runReadyQueue();
return true;
}
// We try the APK, then the system directory.
final boolean distributionSet =
checkAPKDistribution() ||
checkSystemDistribution();
this.state = distributionSet ? STATE_SET : STATE_NONE;
settings.edit().putInt(keyName, this.state).commit();
runReadyQueue();
return distributionSet;
}
/**
* Execute tasks that wanted to run when we were done loading
* the distribution. These tasks are expected to call {@link #exists()}
* to find out whether there's a distribution or not.
*/
private void runReadyQueue() {
Runnable task;
while ((task = onDistributionReady.poll()) != null) {
ThreadUtils.postToBackgroundThread(task);
}
}
/**
* @return true if we copied files out of the APK. Sets distributionDir in that case.
*/
private boolean checkAPKDistribution() {
try {
// First, try copying distribution files out of the APK.
if (copyFiles()) {
// We always copy to the data dir, and we only copy files from
// a 'distribution' subdirectory. Track our dist dir now that
// we know it.
this.distributionDir = new File(getDataDir(), "distribution/");
return true;
}
} catch (IOException e) {
Log.e(LOGTAG, "Error copying distribution files from APK.", e);
}
return false;
}
/**
* @return true if we found a system distribution. Sets distributionDir in that case.
*/
private boolean checkSystemDistribution() {
// If there aren't any distribution files in the APK, look in the /system directory.
final File distDir = getSystemDistributionDir();
if (distDir.exists()) {
this.distributionDir = distDir;
return true;
}
return false;
}
/**
* Copies the /distribution folder out of the APK and into the app's data directory.
* Returns true if distribution files were found and copied.
*/
private boolean copyFiles() throws IOException {
final File applicationPackage = new File(packagePath);
final ZipFile zip = new ZipFile(applicationPackage);
boolean distributionSet = false;
try {
final byte[] buffer = new byte[1024];
final Enumeration<? extends ZipEntry> zipEntries = zip.entries();
while (zipEntries.hasMoreElements()) {
final ZipEntry fileEntry = zipEntries.nextElement();
final String name = fileEntry.getName();
if (fileEntry.isDirectory()) {
// We'll let getDataFile deal with creating the directory hierarchy.
continue;
}
if (!name.startsWith("distribution/")) {
continue;
}
final File outFile = getDataFile(name);
if (outFile == null) {
continue;
}
distributionSet = true;
final InputStream fileStream = zip.getInputStream(fileEntry);
try {
writeStream(fileStream, outFile, fileEntry.getTime(), buffer);
} finally {
fileStream.close();
}
}
} finally {
zip.close();
}
return distributionSet;
}
private void writeStream(InputStream fileStream, File outFile, final long modifiedTime, byte[] buffer)
throws FileNotFoundException, IOException {
final OutputStream outStream = new FileOutputStream(outFile);
try {
int count;
while ((count = fileStream.read(buffer)) > 0) {
outStream.write(buffer, 0, count);
}
outFile.setLastModified(modifiedTime);
} finally {
outStream.close();
}
}
/**
* Return a File instance in the data directory, ensuring
* that the parent exists.
*
* @return null if the parents could not be created.
*/
private File getDataFile(final String name) {
File outFile = new File(getDataDir(), name);
File dir = outFile.getParentFile();
if (!dir.exists()) {
Log.d(LOGTAG, "Creating " + dir.getAbsolutePath());
if (!dir.mkdirs()) {
Log.e(LOGTAG, "Unable to create directories: " + dir.getAbsolutePath());
return null;
}
}
return outFile;
}
/**
* After calling this method, either <code>distributionDir</code>
* will be set, or there is no distribution in use.
*
* Only call after init.
*/
private File ensureDistributionDir() {
if (this.distributionDir != null) {
return this.distributionDir;
}
if (this.state != STATE_SET) {
return null;
}
// After init, we know that either we've copied a distribution out of
// the APK, or it exists in /system/.
// Look in each location in turn.
// (This could be optimized by caching the path in shared prefs.)
File copied = new File(getDataDir(), "distribution/");
if (copied.exists()) {
return this.distributionDir = copied;
}
File system = getSystemDistributionDir();
if (system.exists()) {
return this.distributionDir = system;
}
return null;
}
// Shortcut to slurp a file without messing around with streams.
private String getFileContents(File file) throws IOException {
Scanner scanner = null;
@ -366,4 +463,27 @@ public final class Distribution {
private File getSystemDistributionDir() {
return new File("/system/" + context.getPackageName() + "/distribution");
}
/**
* The provided <code>Runnable</code> will be executed after the distribution
* is ready, or discarded if the distribution has already been processed.
*
* Each <code>Runnable</code> will be executed on the background thread.
*/
public void addOnDistributionReadyCallback(Runnable runnable) {
if (state == STATE_UNKNOWN) {
this.onDistributionReady.add(runnable);
} else {
// If we're already initialized, just queue up the runnable.
ThreadUtils.postToBackgroundThread(runnable);
}
}
/**
* A safe way for callers to determine if this Distribution instance
* represents a real live distribution.
*/
public boolean exists() {
return state == STATE_SET;
}
}

View File

@ -14,8 +14,10 @@ import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.fxa.authenticator.AccountPickler;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.sync.FxAccountSyncAdapter;
import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
import org.mozilla.gecko.fxa.tasks.FxAccountCodeResender;
import org.mozilla.gecko.sync.ThreadPool;
import org.mozilla.gecko.sync.Utils;
@ -156,6 +158,38 @@ public class FirefoxAccounts {
return null;
}
/**
* @return
* the {@link State} instance associated with the current account, or <code>null</code> if
* no accounts exist.
*/
public static State getFirefoxAccountState(final Context context) {
final Account account = getFirefoxAccount(context);
if (account == null) {
return null;
}
final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
try {
return fxAccount.getState();
} catch (final Exception ex) {
Logger.warn(LOG_TAG, "Could not get FX account state.", ex);
return null;
}
}
/*
* @param context Android context
* @return the email address associated with the configured Firefox account if one exists; null otherwise.
*/
public static String getFirefoxAccountEmail(final Context context) {
final Account account = getFirefoxAccount(context);
if (account == null) {
return null;
}
return account.name;
}
protected static void putHintsToSync(final Bundle extras, EnumSet<SyncHint> syncHints) {
// stagesToSync and stagesToSkip are allowed to be null.
if (syncHints == null) {
@ -275,4 +309,26 @@ public class FirefoxAccounts {
final String LOCALE = Utils.getLanguageTag(locale);
return res.getString(R.string.fxaccount_link_old_firefox, VERSION, OS, LOCALE);
}
/**
* Resends the account verification email, and displays an appropriate
* toast on both send success and failure. Note that because the underlying implementation
* uses {@link AsyncTask}, the provided context must be UI-capable, and this
* method called from the UI thread (see
* {@link org.mozilla.gecko.fxa.tasks.FxAccountCodeResender#resendCode(Context, AndroidFxAccount)}
* for more).
*
* @param context a UI-capable Android context.
* @return true if an account exists, false otherwise.
*/
public static boolean resendVerificationEmail(final Context context) {
final Account account = getFirefoxAccount(context);
if (account == null) {
return false;
}
final AndroidFxAccount fxAccount = new AndroidFxAccount(context, account);
FxAccountCodeResender.resendCode(context, fxAccount);
return true;
}
}

View File

@ -19,10 +19,10 @@ import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.background.fxa.PasswordStretcher;
import org.mozilla.gecko.background.fxa.QuickPasswordStretcher;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.ProgressDisplay;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.Engaged;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.tasks.FxAccountSetupTask.ProgressDisplay;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.setup.Constants;
import org.mozilla.gecko.sync.setup.activities.ActivityUtils;

View File

@ -4,21 +4,15 @@
package org.mozilla.gecko.fxa.activities;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.Engaged;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.Action;
import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
import org.mozilla.gecko.fxa.tasks.FxAccountCodeResender;
import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
import android.accounts.Account;
@ -28,7 +22,6 @@ import android.os.Bundle;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;
import android.widget.Toast;
/**
* Activity which displays account created successfully screen to the user, and
@ -164,76 +157,8 @@ public class FxAccountConfirmAccountActivity extends FxAccountAbstractActivity i
resendLink.setClickable(resendLinkShouldBeEnabled);
}
public static class FxAccountResendCodeTask extends FxAccountSetupTask<Void> {
protected static final String LOG_TAG = FxAccountResendCodeTask.class.getSimpleName();
protected final byte[] sessionToken;
public FxAccountResendCodeTask(Context context, byte[] sessionToken, FxAccountClient client, RequestDelegate<Void> delegate) {
super(context, null, client, delegate);
this.sessionToken = sessionToken;
}
@Override
protected InnerRequestDelegate<Void> doInBackground(Void... arg0) {
try {
client.resendCode(sessionToken, innerDelegate);
latch.await();
return innerDelegate;
} catch (Exception e) {
Logger.error(LOG_TAG, "Got exception signing in.", e);
delegate.handleError(e);
}
return null;
}
}
protected static class ResendCodeDelegate implements RequestDelegate<Void> {
public final Context context;
public ResendCodeDelegate(Context context) {
this.context = context;
}
@Override
public void handleError(Exception e) {
Logger.warn(LOG_TAG, "Got exception requesting fresh confirmation link; ignoring.", e);
Toast.makeText(context, R.string.fxaccount_confirm_account_verification_link_not_sent, Toast.LENGTH_LONG).show();
}
@Override
public void handleFailure(FxAccountClientRemoteException e) {
handleError(e);
}
@Override
public void handleSuccess(Void result) {
Toast.makeText(context, R.string.fxaccount_confirm_account_verification_link_sent, Toast.LENGTH_SHORT).show();
}
}
public static void resendCode(Context context, AndroidFxAccount fxAccount) {
RequestDelegate<Void> delegate = new ResendCodeDelegate(context);
byte[] sessionToken;
try {
sessionToken = ((Engaged) fxAccount.getState()).getSessionToken();
} catch (Exception e) {
delegate.handleError(e);
return;
}
if (sessionToken == null) {
delegate.handleError(new IllegalStateException("sessionToken should not be null"));
return;
}
Executor executor = Executors.newSingleThreadExecutor();
FxAccountClient client = new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
new FxAccountResendCodeTask(context, sessionToken, client, delegate).execute();
}
@Override
public void onClick(View v) {
resendCode(this, fxAccount);
FxAccountCodeResender.resendCode(this, fxAccount);
}
}

View File

@ -21,7 +21,7 @@ import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.background.fxa.PasswordStretcher;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.FxAccountCreateAccountTask;
import org.mozilla.gecko.fxa.tasks.FxAccountCreateAccountTask;
import android.app.AlertDialog;
import android.app.Dialog;

View File

@ -16,7 +16,7 @@ import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.background.fxa.PasswordStretcher;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.FxAccountSignInTask;
import org.mozilla.gecko.fxa.tasks.FxAccountSignInTask;
import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
import android.content.Intent;

View File

@ -17,6 +17,7 @@ import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.Married;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.sync.FxAccountSyncStatusHelper;
import org.mozilla.gecko.fxa.tasks.FxAccountCodeResender;
import org.mozilla.gecko.sync.SyncConfiguration;
import android.accounts.Account;
@ -142,7 +143,7 @@ public class FxAccountStatusFragment extends PreferenceFragment implements OnPre
}
if (preference == needsVerificationPreference) {
FxAccountConfirmAccountActivity.resendCode(getActivity().getApplicationContext(), fxAccount);
FxAccountCodeResender.resendCode(getActivity().getApplicationContext(), fxAccount);
Intent intent = new Intent(getActivity(), FxAccountConfirmAccountActivity.class);
// Per http://stackoverflow.com/a/8992365, this triggers a known bug with

View File

@ -18,11 +18,11 @@ import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.background.fxa.PasswordStretcher;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.FxAccountSignInTask;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.Engaged;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import org.mozilla.gecko.fxa.tasks.FxAccountSignInTask;
import org.mozilla.gecko.sync.setup.activities.ActivityUtils;
import android.os.Bundle;

View File

@ -0,0 +1,108 @@
/* 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.fxa.tasks;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import org.mozilla.gecko.R;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
import org.mozilla.gecko.background.fxa.FxAccountClient20;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.fxa.authenticator.AndroidFxAccount;
import org.mozilla.gecko.fxa.login.Engaged;
import android.content.Context;
import android.widget.Toast;
/**
* A helper class that provides a simple interface for requesting
* a Firefox Account verification email to be resent.
*/
public class FxAccountCodeResender {
private static final String LOG_TAG = FxAccountCodeResender.class.getSimpleName();
private static class FxAccountResendCodeTask extends FxAccountSetupTask<Void> {
protected static final String LOG_TAG = FxAccountResendCodeTask.class.getSimpleName();
protected final byte[] sessionToken;
public FxAccountResendCodeTask(Context context, byte[] sessionToken, FxAccountClient client, RequestDelegate<Void> delegate) {
super(context, null, client, delegate);
this.sessionToken = sessionToken;
}
@Override
protected InnerRequestDelegate<Void> doInBackground(Void... arg0) {
try {
client.resendCode(sessionToken, innerDelegate);
latch.await();
return innerDelegate;
} catch (Exception e) {
Logger.error(LOG_TAG, "Got exception signing in.", e);
delegate.handleError(e);
}
return null;
}
}
private static class ResendCodeDelegate implements RequestDelegate<Void> {
public final Context context;
public ResendCodeDelegate(Context context) {
this.context = context;
}
@Override
public void handleError(Exception e) {
Logger.warn(LOG_TAG, "Got exception requesting fresh confirmation link; ignoring.", e);
Toast.makeText(context, R.string.fxaccount_confirm_account_verification_link_not_sent, Toast.LENGTH_LONG).show();
}
@Override
public void handleFailure(FxAccountClientRemoteException e) {
handleError(e);
}
@Override
public void handleSuccess(Void result) {
Toast.makeText(context, R.string.fxaccount_confirm_account_verification_link_sent, Toast.LENGTH_SHORT).show();
}
}
/**
* Resends the account verification email, and displays an appropriate
* toast on both send success and failure. Note that because the underlying implementation
* uses {@link AsyncTask}, the provided context must be UI-capable and
* this method called from the UI thread.
*
* Note that it may actually be possible to run this (and the {@link AsyncTask}) method
* from a background thread - but this hasn't been tested.
*
* @param context A UI-capable Android context.
* @param fxAccount The Firefox Account to resend the code to.
*/
public static void resendCode(Context context, AndroidFxAccount fxAccount) {
RequestDelegate<Void> delegate = new ResendCodeDelegate(context);
byte[] sessionToken;
try {
sessionToken = ((Engaged) fxAccount.getState()).getSessionToken();
} catch (Exception e) {
delegate.handleError(e);
return;
}
if (sessionToken == null) {
delegate.handleError(new IllegalStateException("sessionToken should not be null"));
return;
}
Executor executor = Executors.newSingleThreadExecutor();
FxAccountClient client = new FxAccountClient20(fxAccount.getAccountServerURI(), executor);
new FxAccountResendCodeTask(context, sessionToken, client, delegate).execute();
}
}

View File

@ -0,0 +1,41 @@
/* 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.fxa.tasks;
import java.io.UnsupportedEncodingException;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
import org.mozilla.gecko.background.fxa.PasswordStretcher;
import android.content.Context;
public class FxAccountCreateAccountTask extends FxAccountSetupTask<LoginResponse> {
private static final String LOG_TAG = FxAccountCreateAccountTask.class.getSimpleName();
protected final byte[] emailUTF8;
protected final PasswordStretcher passwordStretcher;
public FxAccountCreateAccountTask(Context context, ProgressDisplay progressDisplay, String email, PasswordStretcher passwordStretcher, FxAccountClient client, RequestDelegate<LoginResponse> delegate) throws UnsupportedEncodingException {
super(context, progressDisplay, client, delegate);
this.emailUTF8 = email.getBytes("UTF-8");
this.passwordStretcher = passwordStretcher;
}
@Override
protected InnerRequestDelegate<LoginResponse> doInBackground(Void... arg0) {
try {
client.createAccountAndGetKeys(emailUTF8, passwordStretcher, innerDelegate);
latch.await();
return innerDelegate;
} catch (Exception e) {
Logger.error(LOG_TAG, "Got exception logging in.", e);
delegate.handleError(e);
}
return null;
}
}

View File

@ -2,18 +2,15 @@
* 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.fxa.activities;
package org.mozilla.gecko.fxa.tasks;
import java.io.UnsupportedEncodingException;
import java.util.concurrent.CountDownLatch;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.background.fxa.PasswordStretcher;
import org.mozilla.gecko.fxa.activities.FxAccountSetupTask.InnerRequestDelegate;
import org.mozilla.gecko.fxa.tasks.FxAccountSetupTask.InnerRequestDelegate;
import android.content.Context;
import android.os.AsyncTask;
@ -27,7 +24,7 @@ import android.os.AsyncTask;
* We really want to avoid making a threading mistake that brings down the whole
* process.
*/
abstract class FxAccountSetupTask<T> extends AsyncTask<Void, Void, InnerRequestDelegate<T>> {
public abstract class FxAccountSetupTask<T> extends AsyncTask<Void, Void, InnerRequestDelegate<T>> {
private static final String LOG_TAG = FxAccountSetupTask.class.getSimpleName();
public interface ProgressDisplay {
@ -117,56 +114,4 @@ abstract class FxAccountSetupTask<T> extends AsyncTask<Void, Void, InnerRequestD
latch.countDown();
}
}
public static class FxAccountCreateAccountTask extends FxAccountSetupTask<LoginResponse> {
private static final String LOG_TAG = FxAccountCreateAccountTask.class.getSimpleName();
protected final byte[] emailUTF8;
protected final PasswordStretcher passwordStretcher;
public FxAccountCreateAccountTask(Context context, ProgressDisplay progressDisplay, String email, PasswordStretcher passwordStretcher, FxAccountClient client, RequestDelegate<LoginResponse> delegate) throws UnsupportedEncodingException {
super(context, progressDisplay, client, delegate);
this.emailUTF8 = email.getBytes("UTF-8");
this.passwordStretcher = passwordStretcher;
}
@Override
protected InnerRequestDelegate<LoginResponse> doInBackground(Void... arg0) {
try {
client.createAccountAndGetKeys(emailUTF8, passwordStretcher, innerDelegate);
latch.await();
return innerDelegate;
} catch (Exception e) {
Logger.error(LOG_TAG, "Got exception logging in.", e);
delegate.handleError(e);
}
return null;
}
}
public static class FxAccountSignInTask extends FxAccountSetupTask<LoginResponse> {
protected static final String LOG_TAG = FxAccountSignInTask.class.getSimpleName();
protected final byte[] emailUTF8;
protected final PasswordStretcher passwordStretcher;
public FxAccountSignInTask(Context context, ProgressDisplay progressDisplay, String email, PasswordStretcher passwordStretcher, FxAccountClient client, RequestDelegate<LoginResponse> delegate) throws UnsupportedEncodingException {
super(context, progressDisplay, client, delegate);
this.emailUTF8 = email.getBytes("UTF-8");
this.passwordStretcher = passwordStretcher;
}
@Override
protected InnerRequestDelegate<LoginResponse> doInBackground(Void... arg0) {
try {
client.loginAndGetKeys(emailUTF8, passwordStretcher, innerDelegate);
latch.await();
return innerDelegate;
} catch (Exception e) {
Logger.error(LOG_TAG, "Got exception signing in.", e);
delegate.handleError(e);
}
return null;
}
}
}

View File

@ -0,0 +1,41 @@
/* 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.fxa.tasks;
import java.io.UnsupportedEncodingException;
import org.mozilla.gecko.background.common.log.Logger;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
import org.mozilla.gecko.background.fxa.PasswordStretcher;
import android.content.Context;
public class FxAccountSignInTask extends FxAccountSetupTask<LoginResponse> {
protected static final String LOG_TAG = FxAccountSignInTask.class.getSimpleName();
protected final byte[] emailUTF8;
protected final PasswordStretcher passwordStretcher;
public FxAccountSignInTask(Context context, ProgressDisplay progressDisplay, String email, PasswordStretcher passwordStretcher, FxAccountClient client, RequestDelegate<LoginResponse> delegate) throws UnsupportedEncodingException {
super(context, progressDisplay, client, delegate);
this.emailUTF8 = email.getBytes("UTF-8");
this.passwordStretcher = passwordStretcher;
}
@Override
protected InnerRequestDelegate<LoginResponse> doInBackground(Void... arg0) {
try {
client.loginAndGetKeys(emailUTF8, passwordStretcher, innerDelegate);
latch.await();
return innerDelegate;
} catch (Exception e) {
Logger.error(LOG_TAG, "Got exception signing in.", e);
delegate.handleError(e);
}
return null;
}
}

View File

@ -22,8 +22,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.Distribution;
import org.mozilla.gecko.Distribution.DistributionDescriptor;
import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoEvent;
@ -34,6 +32,8 @@ import org.mozilla.gecko.background.healthreport.HealthReportDatabaseStorage;
import org.mozilla.gecko.background.healthreport.HealthReportStorage.Field;
import org.mozilla.gecko.background.healthreport.HealthReportStorage.MeasurementFields;
import org.mozilla.gecko.background.healthreport.ProfileInformationCache;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.distribution.Distribution.DistributionDescriptor;
import org.mozilla.gecko.util.GeckoEventListener;
import org.mozilla.gecko.util.ThreadUtils;

View File

@ -229,6 +229,9 @@ size. -->
<!ENTITY pref_update_autodownload_never "Never">
<!ENTITY pref_update_autodownload_always "Always">
<!-- Localization note (help_menu) : This string is used in the main menu-->
<!ENTITY help_menu "Help">
<!ENTITY quit "Quit">
<!ENTITY addons "Add-ons">

View File

@ -145,7 +145,7 @@ gbjar.sources += [
'db/SuggestedSites.java',
'db/TabsProvider.java',
'db/TopSitesCursorWrapper.java',
'Distribution.java',
'distribution/Distribution.java',
'DoorHangerPopup.java',
'DynamicToolbar.java',
'EditBookmarkDialog.java',
@ -361,8 +361,11 @@ gbjar.sources += [
'Tab.java',
'Tabs.java',
'TabsAccessor.java',
'tabspanel/RemoteTabsContainer.java',
'tabspanel/RemoteTabsContainerPanel.java',
'tabspanel/RemoteTabsList.java',
'tabspanel/RemoteTabsPanel.java',
'tabspanel/RemoteTabsSetupPanel.java',
'tabspanel/RemoteTabsVerificationPanel.java',
'tabspanel/TabsPanel.java',
'tabspanel/TabsTray.java',
'Telemetry.java',

View File

@ -10,12 +10,14 @@ import java.util.Locale;
import org.mozilla.gecko.BrowserLocaleManager;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.LocaleManager;
import org.mozilla.gecko.PrefsHelper;
import org.mozilla.gecko.R;
import android.app.ActionBar;
import android.app.Activity;
import android.content.Context;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
@ -33,6 +35,20 @@ import android.view.ViewConfiguration;
*/
public class GeckoPreferenceFragment extends PreferenceFragment {
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Log.d(LOGTAG, "onConfigurationChanged: " + newConfig.locale);
final Activity context = getActivity();
final LocaleManager localeManager = BrowserLocaleManager.getInstance();
final Locale changed = localeManager.onSystemConfigurationChanged(context, getResources(), newConfig, lastLocale);
if (changed != null) {
applyLocale(changed);
}
}
private static final String LOGTAG = "GeckoPreferenceFragment";
private int mPrefsRequestId = 0;
private Locale lastLocale = Locale.getDefault();
@ -112,7 +128,13 @@ public class GeckoPreferenceFragment extends PreferenceFragment {
@Override
public void onResume() {
final Locale currentLocale = Locale.getDefault();
// This is a little delicate. Ensure that you do nothing prior to
// super.onResume that you wouldn't do in onCreate.
applyLocale(Locale.getDefault());
super.onResume();
}
private void applyLocale(final Locale currentLocale) {
final Context context = getActivity().getApplicationContext();
BrowserLocaleManager.getInstance().updateConfiguration(context, currentLocale);
@ -129,8 +151,6 @@ public class GeckoPreferenceFragment extends PreferenceFragment {
// Fix the parent title regardless.
updateTitle();
super.onResume();
}
/*

View File

@ -45,6 +45,7 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.preference.CheckBoxPreference;
@ -866,6 +867,24 @@ OnSharedPreferenceChangeListener
return true;
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Log.d(LOGTAG, "onConfigurationChanged: " + newConfig.locale);
if (lastLocale.equals(newConfig.locale)) {
Log.d(LOGTAG, "Old locale same as new locale. Short-circuiting.");
return;
}
final LocaleManager localeManager = BrowserLocaleManager.getInstance();
final Locale changed = localeManager.onSystemConfigurationChanged(this, getResources(), newConfig, lastLocale);
if (changed != null) {
onLocaleChanged(changed);
}
}
/**
* Implementation for the {@link OnSharedPreferenceChangeListener} interface,
* which we use to watch changes in our prefs file.

View File

@ -0,0 +1,20 @@
<?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/. -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="@color/remote_tabs_setup_button_background_hit"/>
<corners android:radius="@dimen/fxaccount_corner_radius"/>
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@color/remote_tabs_setup_button_background"/>
<corners android:radius="@dimen/fxaccount_corner_radius"/>
</shape>
</item>
</selector>

View File

@ -48,20 +48,11 @@
android:visibility="gone"
gecko:tabs="tabs_private"/>
<org.mozilla.gecko.tabspanel.RemoteTabsContainer android:id="@+id/synced_tabs"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:visibility="gone">
<org.mozilla.gecko.tabspanel.RemoteTabsList android:id="@+id/synced_tabs_list"
style="@style/RemoteTabsList"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:paddingLeft="@dimen/tabs_panel_list_padding"
android:paddingRight="@dimen/tabs_panel_list_padding"
android:scrollbarStyle="outsideOverlay"/>
</org.mozilla.gecko.tabspanel.RemoteTabsContainer>
<org.mozilla.gecko.tabspanel.RemoteTabsPanel
android:id="@+id/remote_tabs"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:visibility="gone"/>
</view>

View File

@ -0,0 +1,20 @@
<?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.tabspanel.RemoteTabsContainerPanel
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:visibility="gone">
<org.mozilla.gecko.tabspanel.RemoteTabsList
style="@style/RemoteTabsList"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:paddingLeft="@dimen/tabs_panel_list_padding"
android:paddingRight="@dimen/tabs_panel_list_padding"
android:scrollbarStyle="outsideOverlay"/>
</org.mozilla.gecko.tabspanel.RemoteTabsContainerPanel>

View File

@ -0,0 +1,48 @@
<?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.tabspanel.RemoteTabsSetupPanel
xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/RemoteTabsPanelChild"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<LinearLayout style="@style/RemoteTabsSection"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView style="@style/RemoteTabsItem.TextAppearance.Header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/fxaccount_getting_started_welcome_to_sync"/>
<TextView style="@style/RemoteTabsItem.TextAppearance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/fxaccount_getting_started_description"/>
</LinearLayout>
<LinearLayout style="@style/RemoteTabsSection"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button android:id="@+id/remote_tabs_setup_get_started"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/RemoteTabsItem.Button"
android:text="@string/fxaccount_getting_started_get_started"
android:layout_marginBottom="15dp"/>
<TextView android:id="@+id/remote_tabs_setup_old_sync_link"
android:layout_width="match_parent"
android:layout_height="wrap_content"
style="@style/RemoteTabsItem.TextAppearance.Linkified"
android:text="@string/fxaccount_getting_started_old_firefox"/>
</LinearLayout>
</org.mozilla.gecko.tabspanel.RemoteTabsSetupPanel>

View File

@ -0,0 +1,48 @@
<?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.tabspanel.RemoteTabsVerificationPanel
xmlns:android="http://schemas.android.com/apk/res/android"
style="@style/RemoteTabsPanelChild"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone">
<LinearLayout style="@style/RemoteTabsSection"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView style="@style/RemoteTabsItem.TextAppearance.Header.FXAccounts"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/fxaccount_full_label"
android:layout_marginBottom="0dp"/>
<TextView style="@style/RemoteTabsItem.TextAppearance.Header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/fxaccount_confirm_account_header"
android:layout_marginTop="0dp"/>
<TextView android:id="@+id/remote_tabs_confirm_verification"
style="@style/RemoteTabsItem.TextAppearance"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/fxaccount_confirm_account_verification_link"/>
</LinearLayout>
<LinearLayout style="@style/RemoteTabsSection"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView android:id="@+id/remote_tabs_confirm_resend"
style="@style/RemoteTabsItem.TextAppearance.Linkified.Resend"
android:layout_width="match_parent"
android:text="@string/fxaccount_confirm_account_resend_email"/>
</LinearLayout>
</org.mozilla.gecko.tabspanel.RemoteTabsVerificationPanel>

View File

@ -47,20 +47,11 @@
android:visibility="gone"
gecko:tabs="tabs_private"/>
<org.mozilla.gecko.tabspanel.RemoteTabsContainer android:id="@+id/synced_tabs"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:visibility="gone">
<org.mozilla.gecko.tabspanel.RemoteTabsList android:id="@+id/synced_tabs"
style="@style/RemoteTabsList"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:paddingLeft="@dimen/tabs_panel_list_padding"
android:paddingRight="@dimen/tabs_panel_list_padding"
android:scrollbarStyle="outsideOverlay"/>
</org.mozilla.gecko.tabspanel.RemoteTabsContainer>
<org.mozilla.gecko.tabspanel.RemoteTabsPanel
android:id="@+id/remote_tabs"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:visibility="gone"/>
</view>

View File

@ -108,6 +108,9 @@
android:icon="@drawable/ic_menu_settings"
android:title="@string/settings" />
<item android:id="@+id/help"
android:title="@string/help_menu" />
<item android:id="@+id/exit_guest_session"
android:icon="@drawable/ic_menu_guest"
android:visible="false"

View File

@ -108,6 +108,9 @@
android:icon="@drawable/ic_menu_settings"
android:title="@string/settings" />
<item android:id="@+id/help"
android:title="@string/help_menu" />
<item android:id="@+id/exit_guest_session"
android:icon="@drawable/ic_menu_guest"
android:visible="false"

View File

@ -109,6 +109,9 @@
android:icon="@drawable/ic_menu_settings"
android:title="@string/settings" />
<item android:id="@+id/help"
android:title="@string/help_menu" />
<item android:id="@+id/exit_guest_session"
android:icon="@drawable/ic_menu_guest"
android:visible="false"

View File

@ -59,6 +59,9 @@
<item android:id="@+id/settings"
android:title="@string/settings" />
<item android:id="@+id/help"
android:title="@string/help_menu" />
<item android:id="@+id/new_guest_session"
android:icon="@drawable/ic_menu_guest"
android:visible="false"

View File

@ -17,4 +17,37 @@
<item name="android:nextFocusUp">@+id/info</item>
</style>
<!-- Remote tabs panel -->
<style name="RemoteTabsPanelChild" parent="RemoteTabsPanelChildBase">
<item name="android:orientation">horizontal</item>
<item name="android:paddingTop">24dp</item>
</style>
<style name="RemoteTabsSection" parent="RemoteTabsSectionBase">
<item name="android:layout_weight">1</item>
</style>
<style name="RemoteTabsItem">
<item name="android:layout_marginBottom">20dp</item>
<item name="android:layout_gravity">left</item>
<item name="android:gravity">left</item>
</style>
<style name="RemoteTabsItem.Button" parent="RemoteTabsItem.ButtonBase">
<item name="android:paddingTop">12dp</item>
<item name="android:paddingBottom">12dp</item>
<item name="android:paddingLeft">64dp</item>
<item name="android:paddingRight">64dp</item>
</style>
<style name="RemoteTabsItem.TextAppearance.Header.FXAccounts">
<item name="android:visibility">gone</item>
</style>
<style name="RemoteTabsItem.TextAppearance.Linkified.Resend">
<item name="android:layout_height">match_parent</item>
<item name="android:gravity">center</item>
<item name="android:layout_gravity">center</item>
</style>
</resources>

View File

@ -96,4 +96,8 @@
<color name="swipe_refresh_orange">#FFFFC26C</color>
<color name="swipe_refresh_white">#FFFFFFFF</color>
<color name="swipe_refresh_orange_dark">#FF9500</color>
<!-- Remote tabs setup -->
<color name="remote_tabs_setup_button_background">#E66000</color>
<color name="remote_tabs_setup_button_background_hit">#D95300</color>
</resources>

View File

@ -448,6 +448,73 @@
<item name="android:groupIndicator">@android:color/transparent</item>
</style>
<!-- Remote tabs panel -->
<style name="RemoteTabsPanelChildBase">
<item name="android:paddingLeft">16dp</item>
<item name="android:paddingRight">16dp</item>
</style>
<style name="RemoteTabsPanelChild" parent="RemoteTabsPanelChildBase">
<item name="android:orientation">vertical</item>
<item name="android:paddingTop">62dp</item>
</style>
<style name="RemoteTabsSectionBase">
<item name="android:orientation">vertical</item>
<item name="android:layout_marginLeft">16dp</item>
<item name="android:layout_marginRight">16dp</item>
</style>
<style name="RemoteTabsSection" parent="RemoteTabsSectionBase">
<!-- We set values in landscape. -->
</style>
<style name="RemoteTabsSection.Resend" parent="RemoteTabsSectionBase">
<!-- We set values in landscape. -->
</style>
<style name="RemoteTabsItem">
<item name="android:layout_marginBottom">28dp</item>
<item name="android:layout_gravity">center</item>
<item name="android:gravity">center</item>
</style>
<style name="RemoteTabsItem.ButtonBase">
<item name="android:background">@drawable/remote_tabs_setup_button_background</item>
<item name="android:textColor">#FFFEFF</item>
<item name="android:textSize">20sp</item>
</style>
<style name="RemoteTabsItem.Button" parent="RemoteTabsItem.ButtonBase">
<item name="android:paddingTop">18dp</item>
<item name="android:paddingBottom">18dp</item>
<item name="android:paddingLeft">72dp</item>
<item name="android:paddingRight">72dp</item>
</style>
<style name="RemoteTabsItem.TextAppearance">
<item name="android:textColor">#C0C9D0</item>
<item name="android:textSize">16sp</item>
</style>
<style name="RemoteTabsItem.TextAppearance.Header">
<item name="android:textSize">20sp</item>
</style>
<style name="RemoteTabsItem.TextAppearance.Header.FXAccounts">
<!-- We change these values on landscape. -->
</style>
<style name="RemoteTabsItem.TextAppearance.Linkified">
<item name="android:clickable">true</item>
<item name="android:focusable">true</item>
<item name="android:textColor">#0292D6</item>
</style>
<style name="RemoteTabsItem.TextAppearance.Linkified.Resend">
<item name="android:layout_height">wrap_content</item>
</style>
<!-- TabsTray Row -->
<style name="TabRowTextAppearance">
<item name="android:textColor">#FFFFFFFF</item>

View File

@ -60,6 +60,10 @@
<string name="url_bar_default_text">&url_bar_default_text;</string>
<!-- https://support.mozilla.org/1/mobile/%VERSION%/%OS%/%LOCALE%/ -->
<string name="help_link">https://support.mozilla.org/1/mobile/&formatS1;/&formatS2;/&formatS3;/</string>
<string name="help_menu">&help_menu;</string>
<string name="quit">&quit;</string>
<string name="bookmark">&bookmark;</string>
<string name="bookmark_added">&bookmark_added;</string>

View File

@ -21,7 +21,7 @@ import android.view.ViewGroup;
* Provides a container to wrap the list of synced tabs and provide swipe-to-refresh support. The
* only child view should be an instance of {@link RemoteTabsList}.
*/
public class RemoteTabsContainer extends GeckoSwipeRefreshLayout
public class RemoteTabsContainerPanel extends GeckoSwipeRefreshLayout
implements TabsPanel.PanelView {
private static final String[] STAGES_TO_SYNC_ON_REFRESH = new String[] { "tabs" };
@ -29,7 +29,10 @@ public class RemoteTabsContainer extends GeckoSwipeRefreshLayout
private final RemoteTabsSyncObserver syncListener;
private RemoteTabsList list;
public RemoteTabsContainer(Context context, AttributeSet attrs) {
// Whether or not a sync status listener is attached.
private boolean isListening = false;
public RemoteTabsContainerPanel(Context context, AttributeSet attrs) {
super(context, attrs);
this.context = context;
this.syncListener = new RemoteTabsSyncObserver();
@ -67,15 +70,21 @@ public class RemoteTabsContainer extends GeckoSwipeRefreshLayout
@Override
public void show() {
setVisibility(VISIBLE);
TabsAccessor.getTabs(context, list);
FirefoxAccounts.addSyncStatusListener(syncListener);
if (!isListening) {
isListening = true;
FirefoxAccounts.addSyncStatusListener(syncListener);
}
setVisibility(View.VISIBLE);
}
@Override
public void hide() {
setVisibility(GONE);
FirefoxAccounts.removeSyncStatusListener(syncListener);
setVisibility(View.GONE);
if (isListening) {
isListening = false;
FirefoxAccounts.removeSyncStatusListener(syncListener);
}
}
@Override
@ -96,7 +105,7 @@ public class RemoteTabsContainer extends GeckoSwipeRefreshLayout
private class RemoteTabsSyncObserver implements FirefoxAccounts.SyncStatusListener {
@Override
public Context getContext() {
return RemoteTabsContainer.this.getContext();
return RemoteTabsContainerPanel.this.getContext();
}
@Override

View File

@ -23,7 +23,7 @@ import android.widget.ExpandableListView;
import android.widget.SimpleExpandableListAdapter;
/**
* The actual list of synced tabs. This serves as the only child view of {@link RemoteTabsContainer}
* The actual list of synced tabs. This serves as the only child view of {@link RemoteTabsContainerPanel}
* so it can be refreshed using a swipe-to-refresh gesture.
*/
class RemoteTabsList extends ExpandableListView

View File

@ -0,0 +1,116 @@
/* 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.tabspanel;
import org.mozilla.gecko.R;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.tabspanel.TabsPanel.PanelView;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.FrameLayout;
/**
* This panel, which is a {@link TabsPanel.PanelView}, chooses which underlying
* PanelView to show based on the current account state, and forwards the appropriate
* calls to the currently visible panel.
*/
class RemoteTabsPanel extends FrameLayout implements PanelView {
private enum RemotePanelType {
SETUP,
VERIFICATION,
CONTAINER
}
private PanelView currentPanel;
private RemotePanelType currentPanelType;
private TabsPanel tabsPanel;
public RemoteTabsPanel(Context context, AttributeSet attrs) {
super(context, attrs);
updateCurrentPanel();
}
@Override
public void setTabsPanel(TabsPanel panel) {
tabsPanel = panel;
currentPanel.setTabsPanel(panel);
}
@Override
public void show() {
updateCurrentPanel();
currentPanel.show();
setVisibility(View.VISIBLE);
}
@Override
public void hide() {
setVisibility(View.GONE);
currentPanel.hide();
}
@Override
public boolean shouldExpand() {
return currentPanel.shouldExpand();
}
private void updateCurrentPanel() {
final RemotePanelType newPanelType = getPanelTypeFromAccountState();
if (newPanelType != currentPanelType) {
// The current panel should be null the first time this is called.
if (currentPanel != null) {
currentPanel.hide();
}
removeAllViews();
currentPanelType = newPanelType;
currentPanel = inflatePanel(currentPanelType);
currentPanel.setTabsPanel(tabsPanel);
addView((View) currentPanel);
}
}
private RemotePanelType getPanelTypeFromAccountState() {
final State accountState = FirefoxAccounts.getFirefoxAccountState(getContext());
if (accountState == null) {
return RemotePanelType.SETUP;
}
if (accountState.getNeededAction() == State.Action.NeedsVerification) {
return RemotePanelType.VERIFICATION;
}
return RemotePanelType.CONTAINER;
}
private PanelView inflatePanel(final RemotePanelType panelType) {
final LayoutInflater inflater = LayoutInflater.from(getContext());
final View inflatedView;
switch (panelType) {
case SETUP:
inflatedView = inflater.inflate(R.layout.remote_tabs_setup_panel, null);
break;
case VERIFICATION:
inflatedView = inflater.inflate(R.layout.remote_tabs_verification_panel, null);
break;
case CONTAINER:
inflatedView = inflater.inflate(R.layout.remote_tabs_container_panel, null);
break;
default:
throw new IllegalArgumentException("Unknown panelType, " + panelType);
}
return (PanelView) inflatedView;
}
}

View File

@ -0,0 +1,89 @@
/* 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.tabspanel;
import java.util.Locale;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.activities.FxAccountCreateAccountActivity;
import org.mozilla.gecko.tabspanel.TabsPanel.PanelView;
import org.mozilla.gecko.util.HardwareUtils;
import android.content.Context;
import android.content.Intent;
import android.util.AttributeSet;
import android.view.View;
import android.widget.LinearLayout;
/**
* A tabs panel which allows a user to get started setting up a Firefox
* Accounts account. Currently used as one sub-panel in a sequence
* contained by the {@link RemoteTabsPanel}.
*/
class RemoteTabsSetupPanel extends LinearLayout implements PanelView {
private TabsPanel tabsPanel;
public RemoteTabsSetupPanel(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
final View setupGetStartedButton = findViewById(R.id.remote_tabs_setup_get_started);
setupGetStartedButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
final Context context = getContext();
// This Activity will redirect to the correct Activity if the
// account is no longer in the setup state.
final Intent intent = new Intent(context, FxAccountCreateAccountActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
});
final View setupOlderVersionLink = findViewById(R.id.remote_tabs_setup_old_sync_link);
setupOlderVersionLink.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
final String url = FirefoxAccounts.getOldSyncUpgradeURL(
getResources(), Locale.getDefault());
Tabs.getInstance().loadUrlInTab(url);
if (tabsPanel != null) {
tabsPanel.autoHidePanel();
}
}
});
}
@Override
public void setTabsPanel(TabsPanel panel) {
tabsPanel = panel;
}
@Override
public void show() {
// We don't have a tablet implementation of this panel.
if (HardwareUtils.isTablet()) {
return;
}
setVisibility(View.VISIBLE);
}
@Override
public void hide() {
setVisibility(View.GONE);
}
@Override
public boolean shouldExpand() {
return getOrientation() == LinearLayout.VERTICAL;
}
}

View File

@ -0,0 +1,119 @@
/* 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.tabspanel;
import org.mozilla.gecko.R;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.login.State;
import org.mozilla.gecko.tabspanel.TabsPanel.PanelView;
import org.mozilla.gecko.util.HardwareUtils;
import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
/**
* A tabs panel which allows a user to get resend the verification email
* to confirm a Firefox Account. Currently used as one sub-panel in a sequence
* contained by the {@link RemoteTabsPanel}.
*/
class RemoteTabsVerificationPanel extends LinearLayout implements PanelView {
private static final String LOG_TAG = RemoteTabsVerificationPanel.class.getSimpleName();
private TabsPanel tabsPanel;
public RemoteTabsVerificationPanel(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
final View resendLink = findViewById(R.id.remote_tabs_confirm_resend);
resendLink.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
final State accountState = FirefoxAccounts.getFirefoxAccountState(getContext());
final State.Action neededAction = accountState.getNeededAction();
if (accountState.getNeededAction() != State.Action.NeedsVerification) {
autoHideTabsPanelOnUnexpectedState("Account expected to need verification " +
"on resend, but action was " + neededAction + " instead.");
return;
}
if (!FirefoxAccounts.resendVerificationEmail(getContext())) {
autoHideTabsPanelOnUnexpectedState("Account DNE when resending verification email.");
return;
}
}
});
}
private void refresh() {
final TextView verificationView =
(TextView) findViewById(R.id.remote_tabs_confirm_verification);
final String email = FirefoxAccounts.getFirefoxAccountEmail(getContext());
if (email == null) {
autoHideTabsPanelOnUnexpectedState("Account email DNE on View refresh.");
return;
}
final String text = getResources().getString(
R.string.fxaccount_confirm_account_verification_link, email);
verificationView.setText(text);
}
/**
* Hides the tabs panel and logs the given String.
*
* As the name suggests, this method should be only be used for unexpected states!
* We hide the tabs panel on unexpected states as the best of several evils - hiding
* the tabs panel communicates to the user, "Hey, that's a strange bug!" and, if they're
* curious enough, will reopen the RemoteTabsPanel, refreshing its contents. Since we're
* in a strange state, we may already be screwed, but it's better than some alternatives like:
* * Crashing
* * Hiding the resources which allow invalid state (e.g. resend link, email text)
* * Attempting to refresh the RemoteTabsPanel, possibly starting an infinite loop.
*
* @param log The message to log.
*/
private void autoHideTabsPanelOnUnexpectedState(final String log) {
Log.w(LOG_TAG, "Unexpected state: " + log + " Closing the tabs panel.");
if (tabsPanel != null) {
tabsPanel.autoHidePanel();
}
}
@Override
public void setTabsPanel(TabsPanel panel) {
tabsPanel = panel;
}
@Override
public void show() {
// We don't have a tablet implementation of this panel.
if (HardwareUtils.isTablet()) {
return;
}
refresh();
setVisibility(View.VISIBLE);
}
@Override
public void hide() {
setVisibility(View.GONE);
}
@Override
public boolean shouldExpand() {
return getOrientation() == LinearLayout.VERTICAL;
}
}

View File

@ -7,6 +7,7 @@ package org.mozilla.gecko.tabspanel;
import org.mozilla.gecko.GeckoApp;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoAppShell.AppStateListener;
import org.mozilla.gecko.GeckoApplication;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.LightweightTheme;
@ -23,7 +24,6 @@ import android.os.Build;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageButton;
@ -63,6 +63,7 @@ public class TabsPanel extends LinearLayout
private PanelView mPanelRemote;
private RelativeLayout mFooter;
private TabsLayoutChangeListener mLayoutChangeListener;
private AppStateListener mAppStateListener;
private IconTabWidget mTabWidget;
private static ImageButton mAddTab;
@ -90,6 +91,24 @@ public class TabsPanel extends LinearLayout
LayoutInflater.from(context).inflate(R.layout.tabs_panel, this);
initialize();
mAppStateListener = new AppStateListener() {
@Override
public void onResume() {
if (mPanel == mPanelRemote) {
// Refresh the remote panel.
mPanelRemote.show();
}
}
@Override
public void onOrientationChanged() {
// Remote panel is already refreshed by chrome refresh.
}
@Override
public void onPause() {}
};
}
private void initialize() {
@ -102,7 +121,7 @@ public class TabsPanel extends LinearLayout
mPanelPrivate = (PanelView) findViewById(R.id.private_tabs);
mPanelPrivate.setTabsPanel(this);
mPanelRemote = (PanelView) findViewById(R.id.synced_tabs);
mPanelRemote = (PanelView) findViewById(R.id.remote_tabs);
mPanelRemote.setTabsPanel(this);
mFooter = (RelativeLayout) findViewById(R.id.tabs_panel_footer);
@ -174,12 +193,14 @@ public class TabsPanel extends LinearLayout
public void onAttachedToWindow() {
super.onAttachedToWindow();
mTheme.addListener(this);
mActivity.addAppStateListener(mAppStateListener);
}
@Override
public void onDetachedFromWindow() {
super.onDetachedFromWindow();
mTheme.removeListener(this);
mActivity.removeAppStateListener(mAppStateListener);
}
@Override

View File

@ -9,8 +9,8 @@ import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.Actions;
import org.mozilla.gecko.Distribution;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.util.ThreadUtils;
import android.app.Activity;

View File

@ -120,6 +120,10 @@ public final class ThreadUtils {
assertOnThread(getUiThread(), AssertBehavior.THROW);
}
public static void assertNotOnUiThread() {
assertNotOnThread(getUiThread(), AssertBehavior.THROW);
}
@RobocopTarget
public static void assertOnGeckoThread() {
assertOnThread(sGeckoThread, AssertBehavior.THROW);
@ -134,11 +138,19 @@ public final class ThreadUtils {
}
public static void assertOnThread(final Thread expectedThread, AssertBehavior behavior) {
assertOnThreadComparison(expectedThread, behavior, true);
}
public static void assertNotOnThread(final Thread expectedThread, AssertBehavior behavior) {
assertOnThreadComparison(expectedThread, behavior, false);
}
private static void assertOnThreadComparison(final Thread expectedThread, AssertBehavior behavior, boolean expected) {
final Thread currentThread = Thread.currentThread();
final long currentThreadId = currentThread.getId();
final long expectedThreadId = expectedThread.getId();
if (currentThreadId == expectedThreadId) {
if ((currentThreadId == expectedThreadId) == expected) {
return;
}

View File

@ -21,7 +21,7 @@
package org.mozilla.gecko.widget;
// Mozilla: New import
import org.mozilla.gecko.Distribution;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.GeckoProfile;
import java.io.File;

View File

@ -1435,11 +1435,19 @@ var BrowserApp = {
case "Tab:Load": {
let data = JSON.parse(aData);
let url = data.url;
let flags;
if (/^[0-9]+$/.test(url)) {
// If the query is a number, force a search (see bug 993705; workaround for bug 693808).
url = URIFixup.keywordToURI(url).spec;
} else {
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP |
Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
}
// Pass LOAD_FLAGS_DISALLOW_INHERIT_OWNER to prevent any loads from
// inheriting the currently loaded document's principal.
let flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP |
Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS;
if (data.userEntered) {
flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_OWNER;
}
@ -1456,7 +1464,6 @@ var BrowserApp = {
desktopMode: (data.desktopMode === true)
};
let url = data.url;
if (data.engine) {
let engine = Services.search.getEngineByName(data.engine);
if (engine) {

View File

@ -4,6 +4,7 @@
android:clearTaskOnLaunch="true"
android:taskAffinity="@ANDROID_PACKAGE_NAME@.FXA"
android:name="org.mozilla.gecko.fxa.activities.FxAccountStatusActivity"
android:configChanges="locale|layoutDirection"
android:windowSoftInputMode="adjustResize">
<!-- Adding a launcher will make this activity appear on the
Apps screen, which we only want when testing. -->
@ -19,6 +20,7 @@
android:clearTaskOnLaunch="true"
android:taskAffinity="@ANDROID_PACKAGE_NAME@.FXA"
android:name="org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity"
android:configChanges="locale|layoutDirection"
android:windowSoftInputMode="adjustResize">
<!-- Adding a launcher will make this activity appear on the
Apps screen, which we only want when testing. -->
@ -31,12 +33,14 @@
<activity
android:theme="@style/FxAccountTheme"
android:name="org.mozilla.gecko.fxa.activities.FxAccountCreateAccountActivity"
android:configChanges="locale|layoutDirection"
android:windowSoftInputMode="adjustResize">
</activity>
<activity
android:theme="@style/FxAccountTheme"
android:name="org.mozilla.gecko.fxa.activities.FxAccountConfirmAccountActivity"
android:configChanges="locale|layoutDirection"
android:noHistory="true"
android:windowSoftInputMode="adjustResize">
</activity>
@ -44,12 +48,14 @@
<activity
android:theme="@style/FxAccountTheme"
android:name="org.mozilla.gecko.fxa.activities.FxAccountSignInActivity"
android:configChanges="locale|layoutDirection"
android:windowSoftInputMode="adjustResize">
</activity>
<activity
android:theme="@style/FxAccountTheme"
android:name="org.mozilla.gecko.fxa.activities.FxAccountVerifiedAccountActivity"
android:configChanges="locale|layoutDirection"
android:noHistory="true"
android:windowSoftInputMode="adjustResize">
</activity>
@ -57,12 +63,14 @@
<activity
android:theme="@style/FxAccountTheme"
android:name="org.mozilla.gecko.fxa.activities.FxAccountUpdateCredentialsActivity"
android:configChanges="locale|layoutDirection"
android:windowSoftInputMode="adjustResize">
</activity>
<activity
android:theme="@style/FxAccountTheme"
android:name="org.mozilla.gecko.fxa.activities.FxAccountCreateAccountNotAllowedActivity"
android:configChanges="locale|layoutDirection"
android:noHistory="true"
android:windowSoftInputMode="adjustResize">
</activity>

View File

@ -79,7 +79,7 @@
android:excludeFromRecents="true"
android:icon="@drawable/icon"
android:label="@string/sync_title_send_tab"
android:configChanges="keyboardHidden|orientation|screenSize"
android:configChanges="keyboardHidden|orientation|screenSize|locale|layoutDirection"
android:windowSoftInputMode="adjustResize|stateHidden"
android:taskAffinity="org.mozilla.gecko.sync.setup"
android:name="org.mozilla.gecko.sync.setup.activities.SendTabActivity" >

View File

@ -8,6 +8,11 @@
var kSignonBundle;
var showingPasswords = false;
var dateFormatter = new Intl.DateTimeFormat(undefined,
{ day: "numeric", month: "short", year: "numeric" });
var dateAndTimeFormatter = new Intl.DateTimeFormat(undefined,
{ day: "numeric", month: "short", year: "numeric",
hour: "numeric", minute: "numeric" });
function SignonsStartup() {
kSignonBundle = document.getElementById("signonBundle");
@ -41,6 +46,7 @@ var signonsTreeView = {
getProgressMode : function(row,column) {},
getCellValue : function(row,column) {},
getCellText : function(row,column) {
var time;
var signon = this._filterSet.length ? this._filterSet[row] : signons[row];
switch (column.id) {
case "siteCol":
@ -51,6 +57,17 @@ var signonsTreeView = {
return signon.username || "";
case "passwordCol":
return signon.password || "";
case "timeCreatedCol":
time = new Date(signon.timeCreated);
return dateFormatter.format(time);
case "timeLastUsedCol":
time = new Date(signon.timeLastUsed);
return dateAndTimeFormatter.format(time);
case "timePasswordChangedCol":
time = new Date(signon.timePasswordChanged);
return dateFormatter.format(time);
case "timesUsedCol":
return signon.timesUsed;
default:
return "";
}
@ -77,6 +94,7 @@ function LoadSignons() {
} catch (e) {
signons = [];
}
signons.forEach(login => login.QueryInterface(Components.interfaces.nsILoginMetaInfo));
signonsTreeView.rowCount = signons.length;
// sort and display the table
@ -197,6 +215,14 @@ function getColumnByName(column) {
return document.getElementById("userCol");
case "password":
return document.getElementById("passwordCol");
case "timeCreated":
return document.getElementById("timeCreatedCol");
case "timeLastUsed":
return document.getElementById("timeLastUsedCol");
case "timePasswordChanged":
return document.getElementById("timePasswordChangedCol");
case "timesUsed":
return document.getElementById("timesUsedCol");
}
}

View File

@ -54,21 +54,41 @@
<label control="signonsTree" id="signonsIntro"/>
<separator class="thin"/>
<tree id="signonsTree" flex="1" style="height: 20em;" hidecolumnpicker="true"
<tree id="signonsTree" flex="1"
width="750"
style="height: 20em;"
onkeypress="HandleSignonKeyPress(event)"
onselect="SignonSelected();"
context="signonsTreeContextMenu">
<treecols>
<treecol id="siteCol" label="&treehead.site.label;" flex="5"
<treecol id="siteCol" label="&treehead.site.label;" flex="40"
onclick="SignonColumnSort('hostname');" persist="width"
ignoreincolumnpicker="true"
sortDirection="ascending"/>
<splitter class="tree-splitter"/>
<treecol id="userCol" label="&treehead.username.label;" flex="2"
<treecol id="userCol" label="&treehead.username.label;" flex="25"
ignoreincolumnpicker="true"
onclick="SignonColumnSort('username');" persist="width"/>
<splitter class="tree-splitter"/>
<treecol id="passwordCol" label="&treehead.password.label;" flex="2"
<treecol id="passwordCol" label="&treehead.password.label;" flex="15"
ignoreincolumnpicker="true"
onclick="SignonColumnSort('password');" persist="width"
hidden="true"/>
<splitter class="tree-splitter"/>
<treecol id="timeCreatedCol" label="&treehead.timeCreated.label;" flex="10"
onclick="SignonColumnSort('timeCreated');" persist="width hidden"
hidden="true"/>
<splitter class="tree-splitter"/>
<treecol id="timeLastUsedCol" label="&treehead.timeLastUsed.label;" flex="20"
onclick="SignonColumnSort('timeLastUsed');" persist="width hidden"/>
<splitter class="tree-splitter"/>
<treecol id="timePasswordChangedCol" label="&treehead.timePasswordChanged.label;" flex="10"
onclick="SignonColumnSort('timePasswordChanged');" persist="width hidden"/>
<splitter class="tree-splitter"/>
<treecol id="timesUsedCol" label="&treehead.timesUsed.label;" flex="1"
onclick="SignonColumnSort('timesUsed');" persist="width hidden"
hidden="true"/>
<splitter class="tree-splitter"/>
</treecols>
<treechildren/>
</tree>

View File

@ -169,18 +169,40 @@ function SortTree(tree, view, table, column, lastSortColumn, lastSortAscending,
// determine if sort is to be ascending or descending
var ascending = (column == lastSortColumn) ? !lastSortAscending : true;
// do the sort
var compareFunc;
if (ascending) {
compareFunc = function compare(first, second) {
return CompareLowerCase(first[column], second[column]);
}
} else {
compareFunc = function compare(first, second) {
return CompareLowerCase(second[column], first[column]);
function compareFunc(a, b) {
var valA, valB;
switch (column) {
case "hostname":
var realmA = a.httpRealm;
var realmB = b.httpRealm;
realmA = realmA == null ? "" : realmA.toLowerCase();
realmB = realmB == null ? "" : realmB.toLowerCase();
valA = a[column].toLowerCase() + realmA;
valB = b[column].toLowerCase() + realmB;
break;
case "username":
case "password":
valA = a[column].toLowerCase();
valB = b[column].toLowerCase();
break;
default:
valA = a[column];
valB = b[column];
}
if (valA < valB)
return -1;
if (valA > valB)
return 1;
return 0;
}
// do the sort
table.sort(compareFunc);
if (!ascending)
table.reverse();
// restore the selection
var selectedRow = -1;
@ -206,28 +228,3 @@ function SortTree(tree, view, table, column, lastSortColumn, lastSortAscending,
return ascending;
}
/**
* Case insensitive string comparator.
*/
function CompareLowerCase(first, second) {
var firstLower, secondLower;
// Are we sorting nsILoginInfo entries or just strings?
if (first.hostname) {
firstLower = first.hostname.toLowerCase();
secondLower = second.hostname.toLowerCase();
} else {
firstLower = first.toLowerCase();
secondLower = second.toLowerCase();
}
if (firstLower < secondLower) {
return -1;
}
if (firstLower > secondLower) {
return 1;
}
return 0;
}

View File

@ -5392,6 +5392,11 @@
"n_buckets": "1000",
"description": "The time (in milliseconds) that it took an 'assign' request to go round trip."
},
"DEVTOOLS_TOOLBOX_OPENED_BOOLEAN": {
"expires_in_version": "never",
"kind": "boolean",
"description": "How many times has the devtool's toolbox been opened?"
},
"DEVTOOLS_OPTIONS_OPENED_BOOLEAN": {
"expires_in_version": "never",
"kind": "boolean",
@ -5492,6 +5497,11 @@
"kind": "boolean",
"description": "How many times has a custom developer tool been opened via the toolbox button?"
},
"DEVTOOLS_TOOLBOX_OPENED_PER_USER_FLAG": {
"expires_in_version": "never",
"kind": "flag",
"description": "How many times has the devtool's toolbox been opened?"
},
"DEVTOOLS_OPTIONS_OPENED_PER_USER_FLAG": {
"expires_in_version": "never",
"kind": "flag",

View File

@ -12,6 +12,11 @@
<!ENTITY treehead.site.label "Site">
<!ENTITY treehead.username.label "Username">
<!ENTITY treehead.password.label "Password">
<!ENTITY treehead.timeCreated.label "First Used">
<!ENTITY treehead.timeLastUsed.label "Last Used">
<!ENTITY treehead.timePasswordChanged.label "Last Changed">
<!ENTITY treehead.timesUsed.label "Times Used">
<!ENTITY remove.label "Remove">
<!ENTITY remove.accesskey "R">
<!ENTITY removeall.label "Remove All">