Merge mozilla-central to mozilla-inbound

This commit is contained in:
Carsten "Tomcat" Book 2015-10-28 10:51:36 +01:00
commit 7df0008d0e
198 changed files with 1540 additions and 711 deletions

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="a26eadc5e1133d5112b6cbc10badbb7670a1090f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="2e89362de40a6c9c36525d36317fa1ae8e67e143"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="a26eadc5e1133d5112b6cbc10badbb7670a1090f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="2e89362de40a6c9c36525d36317fa1ae8e67e143"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>

View File

@ -19,7 +19,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="a26eadc5e1133d5112b6cbc10badbb7670a1090f"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="2e89362de40a6c9c36525d36317fa1ae8e67e143"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="4ace9aaee0e048dfda11bb787646c59982a3dc80"/>

View File

@ -17,7 +17,7 @@
</project>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="a26eadc5e1133d5112b6cbc10badbb7670a1090f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="2e89362de40a6c9c36525d36317fa1ae8e67e143"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="3b9a47b517d345b8d98bc7f787b9a6c2f51ca75d"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="1837d370a964a9719160c79155a07980f2ea4bdf"/>

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="a26eadc5e1133d5112b6cbc10badbb7670a1090f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="2e89362de40a6c9c36525d36317fa1ae8e67e143"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="c9d4fe680662ee44a4bdea42ae00366f5df399cf">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="a26eadc5e1133d5112b6cbc10badbb7670a1090f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="2e89362de40a6c9c36525d36317fa1ae8e67e143"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
<project name="librecovery" path="librecovery" remote="b2g" revision="1b3591a50ed352fc6ddb77462b7b35d0bfa555a3"/>

View File

@ -19,7 +19,7 @@
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="fake-dalvik" path="dalvik" remote="b2g" revision="ca1f327d5acc198bb4be62fa51db2c039032c9ce"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="a26eadc5e1133d5112b6cbc10badbb7670a1090f"/>
<project name="gaia.git" path="gaia" remote="mozillaorg" revision="2e89362de40a6c9c36525d36317fa1ae8e67e143"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="platform_hardware_ril" path="hardware/ril" remote="b2g" revision="4ace9aaee0e048dfda11bb787646c59982a3dc80"/>

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="a26eadc5e1133d5112b6cbc10badbb7670a1090f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="2e89362de40a6c9c36525d36317fa1ae8e67e143"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>

View File

@ -1,9 +1,9 @@
{
"git": {
"git_revision": "a26eadc5e1133d5112b6cbc10badbb7670a1090f",
"git_revision": "2e89362de40a6c9c36525d36317fa1ae8e67e143",
"remote": "https://git.mozilla.org/releases/gaia.git",
"branch": ""
},
"revision": "a99ff14b3258f49f5902775a5e3b849f3455714a",
"revision": "4a596a387a18b019fc2763b3509940d52154d72c",
"repo_path": "integration/gaia-central"
}

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="8d83715f08b7849f16a0dfc88f78d5c3a89c0a54">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="a26eadc5e1133d5112b6cbc10badbb7670a1090f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="2e89362de40a6c9c36525d36317fa1ae8e67e143"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>

View File

@ -18,7 +18,7 @@
<project name="rilproxy" path="rilproxy" remote="b2g" revision="5ef30994f4778b4052e58a4383dbe7890048c87e"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="a26eadc5e1133d5112b6cbc10badbb7670a1090f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="2e89362de40a6c9c36525d36317fa1ae8e67e143"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>
<project name="moztt" path="external/moztt" remote="b2g" revision="3b9a47b517d345b8d98bc7f787b9a6c2f51ca75d"/>
<project name="apitrace" path="external/apitrace" remote="apitrace" revision="1837d370a964a9719160c79155a07980f2ea4bdf"/>

View File

@ -15,7 +15,7 @@
<project name="platform_build" path="build" remote="b2g" revision="c9d4fe680662ee44a4bdea42ae00366f5df399cf">
<copyfile dest="Makefile" src="core/root.mk"/>
</project>
<project name="gaia" path="gaia" remote="mozillaorg" revision="a26eadc5e1133d5112b6cbc10badbb7670a1090f"/>
<project name="gaia" path="gaia" remote="mozillaorg" revision="2e89362de40a6c9c36525d36317fa1ae8e67e143"/>
<project name="fake-libdvm" path="dalvik" remote="b2g" revision="d50ae982b19f42f0b66d08b9eb306be81687869f"/>
<project name="fake-qemu-kernel" path="prebuilts/qemu-kernel" remote="b2g" revision="939b377d55a2f081d94029a30a75d05e5a20daf3"/>
<project name="gonk-misc" path="gonk-misc" remote="b2g" revision="956700d9754349b630a34551750ae6353614b6aa"/>

View File

@ -576,6 +576,12 @@ ContentSearchUIController.prototype = {
}
},
_onMsgSuggestionsCancelled: function () {
if (!this._table.hidden) {
this._hideSuggestions();
}
},
_onMsgState: function (state) {
this.engines = state.engines;
// No point updating the default engine (and the header) if there's no change.

View File

@ -421,13 +421,17 @@ loop.panel = (function(_, mozL10n) {
"room-active": this._isActive()
});
var roomTitle = this.props.room.decryptedContext.roomName ||
this.props.room.decryptedContext.urls[0].description ||
this.props.room.decryptedContext.urls[0].location;
return (
React.createElement("div", {className: roomClasses,
onClick: this.handleClickEntry,
onMouseLeave: this._handleMouseOut,
ref: "roomEntry"},
React.createElement("h2", null,
this.props.room.decryptedContext.roomName
roomTitle
),
React.createElement(RoomEntryContextItem, {
mozLoop: this.props.mozLoop,

View File

@ -421,13 +421,17 @@ loop.panel = (function(_, mozL10n) {
"room-active": this._isActive()
});
var roomTitle = this.props.room.decryptedContext.roomName ||
this.props.room.decryptedContext.urls[0].description ||
this.props.room.decryptedContext.urls[0].location;
return (
<div className={roomClasses}
onClick={this.handleClickEntry}
onMouseLeave={this._handleMouseOut}
ref="roomEntry">
<h2>
{this.props.room.decryptedContext.roomName}
{roomTitle}
</h2>
<RoomEntryContextItem
mozLoop={this.props.mozLoop}

View File

@ -755,8 +755,11 @@ loop.roomViews = (function(mozL10n) {
},
render: function() {
if (this.state.roomName) {
this.setTitle(this.state.roomName);
if (this.state.roomName || this.state.roomContextUrls) {
var roomTitle = this.state.roomName ||
this.state.roomContextUrls[0].description ||
this.state.roomContextUrls[0].location;
this.setTitle(roomTitle);
}
var screenShareData = {

View File

@ -755,8 +755,11 @@ loop.roomViews = (function(mozL10n) {
},
render: function() {
if (this.state.roomName) {
this.setTitle(this.state.roomName);
if (this.state.roomName || this.state.roomContextUrls) {
var roomTitle = this.state.roomName ||
this.state.roomContextUrls[0].description ||
this.state.roomContextUrls[0].location;
this.setTitle(roomTitle);
}
var screenShareData = {

View File

@ -162,9 +162,14 @@ loop.store.TextChatStore = (function() {
// XXX When we add special messages to desktop, we'll need to not post
// multiple changes of room name, only the first. Bug 1171940 should fix this.
if (actionData.roomName) {
var roomName = actionData.roomName;
if (!roomName && actionData.roomContextUrls && actionData.roomContextUrls.length) {
roomName = actionData.roomContextUrls[0].description ||
actionData.roomContextUrls[0].url;
}
this._appendTextChatMessage(CHAT_MESSAGE_TYPES.SPECIAL, {
contentType: CHAT_CONTENT_TYPES.ROOM_NAME,
message: actionData.roomName
message: roomName
});
}

View File

@ -102,6 +102,9 @@ loop.standaloneRoomViews = (function(mozL10n) {
},
render: function() {
var roomName = this.state.roomName ||
this.state.roomContextUrls[0].description ||
this.state.roomContextUrls[0].location;
// The extra scroller div here is for providing a scroll view for shorter
// screens, as the common.css specifies overflow:hidden for the body which
// we need in some places.
@ -110,7 +113,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
React.createElement("div", {className: "handle-user-agent-view"},
React.createElement("div", {className: "info-panel"},
React.createElement("p", {className: "loop-logo-text", title: mozL10n.get("clientShortname2")}),
React.createElement("p", {className: "roomName"}, this.state.roomName),
React.createElement("p", {className: "roomName"}, roomName),
React.createElement("p", {className: "loop-logo"}),
this.state.failureReason ?
@ -444,16 +447,15 @@ loop.standaloneRoomViews = (function(mozL10n) {
componentWillUpdate: function(nextProps, nextState) {
if (this.state.roomState !== ROOM_STATES.READY &&
nextState.roomState === ROOM_STATES.READY) {
var roomName = nextState.roomName || this.state.roomName;
var roomName = nextState.roomName ||
this.state.roomName ||
this.state.roomContextUrls[0].description ||
this.state.roomContextUrls[0].location;
if (roomName) {
this.setTitle(mozL10n.get("standalone_title_with_room_name", {
roomName: roomName,
clientShortname: mozL10n.get("clientShortname2")
}));
} else {
this.setTitle(mozL10n.get("clientShortname2"));
}
this.setTitle(mozL10n.get("standalone_title_with_room_name", {
roomName: roomName,
clientShortname: mozL10n.get("clientShortname2")
}));
}
if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&

View File

@ -102,6 +102,9 @@ loop.standaloneRoomViews = (function(mozL10n) {
},
render: function() {
var roomName = this.state.roomName ||
this.state.roomContextUrls[0].description ||
this.state.roomContextUrls[0].location;
// The extra scroller div here is for providing a scroll view for shorter
// screens, as the common.css specifies overflow:hidden for the body which
// we need in some places.
@ -110,7 +113,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
<div className="handle-user-agent-view">
<div className="info-panel">
<p className="loop-logo-text" title={mozL10n.get("clientShortname2")}></p>
<p className="roomName">{this.state.roomName}</p>
<p className="roomName">{roomName}</p>
<p className="loop-logo" />
{
this.state.failureReason ?
@ -444,16 +447,15 @@ loop.standaloneRoomViews = (function(mozL10n) {
componentWillUpdate: function(nextProps, nextState) {
if (this.state.roomState !== ROOM_STATES.READY &&
nextState.roomState === ROOM_STATES.READY) {
var roomName = nextState.roomName || this.state.roomName;
var roomName = nextState.roomName ||
this.state.roomName ||
this.state.roomContextUrls[0].description ||
this.state.roomContextUrls[0].location;
if (roomName) {
this.setTitle(mozL10n.get("standalone_title_with_room_name", {
roomName: roomName,
clientShortname: mozL10n.get("clientShortname2")
}));
} else {
this.setTitle(mozL10n.get("clientShortname2"));
}
this.setTitle(mozL10n.get("standalone_title_with_room_name", {
roomName: roomName,
clientShortname: mozL10n.get("clientShortname2")
}));
}
if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&

View File

@ -743,6 +743,68 @@ describe("loop.panel", function() {
.eql("New room name");
});
});
describe("Room name priority", function() {
var roomEntry;
beforeEach(function() {
roomEntry = mountRoomEntry({
dispatcher: dispatcher,
room: new loop.store.Room(roomData)
});
});
function setDecryptedContext(newDecryptedContext) {
return new loop.store.Room(_.extend({}, roomData, {
decryptedContext: newDecryptedContext,
ctime: new Date().getTime()
}));
}
it("should use room name by default", function() {
var updatedRoom = setDecryptedContext({
roomName: "Room name",
urls: [
{
description: "Website title",
location: "https://fakeurl.com"
}
]
});
roomEntry.setProps({ room: updatedRoom });
expect(roomEntry.getDOMNode().textContent).eql("Room name");
});
it("should use context title when there's no room title", function() {
var updatedRoom = setDecryptedContext({
urls: [
{
description: "Website title",
location: "https://fakeurl.com"
}
]
});
roomEntry.setProps({ room: updatedRoom });
expect(roomEntry.getDOMNode().textContent).eql("Website title");
});
it("should use website url when there's no room title nor website", function() {
var updatedRoom = setDecryptedContext({
urls: [
{
location: "https://fakeurl.com"
}
]
});
roomEntry.setProps({ room: updatedRoom });
expect(roomEntry.getDOMNode().textContent).eql("https://fakeurl.com");
});
});
});
describe("loop.panel.RoomList", function() {

View File

@ -649,6 +649,48 @@ describe("loop.roomViews", function() {
expect(view.getDOMNode().querySelector(".local video")).not.eql(null);
});
describe("Room name priority", function() {
var roomEntry;
beforeEach(function() {
activeRoomStore.setStoreState({
participants: [{}],
roomState: ROOM_STATES.JOINED,
roomName: "fakeName",
roomContextUrls: [
{
description: "Website title",
location: "https://fakeurl.com"
}
]
});
});
it("should use room name by default", function() {
view = mountTestComponent();
expect(fakeWindow.document.title).to.equal("fakeName");
});
it("should use context title when there's no room title", function() {
activeRoomStore.setStoreState({ roomName: null });
view = mountTestComponent();
expect(fakeWindow.document.title).to.equal("Website title");
});
it("should use website url when there's no room title nor website", function() {
activeRoomStore.setStoreState({
roomName: null,
roomContextUrls: [
{
location: "https://fakeurl.com"
}
]
});
view = mountTestComponent();
expect(fakeWindow.document.title).to.equal("https://fakeurl.com");
});
});
});
describe("Edit Context", function() {

View File

@ -140,7 +140,8 @@ describe("loop.standaloneRoomViews", function() {
it("should display a join room button if the state is not ROOM_JOINED", function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.READY
roomState: ROOM_STATES.READY,
roomName: "fakeName"
});
view = mountTestComponent();
@ -151,7 +152,8 @@ describe("loop.standaloneRoomViews", function() {
it("should dispatch a JoinRoom action when the join room button is clicked", function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.READY
roomState: ROOM_STATES.READY,
roomName: "fakeName"
});
view = mountTestComponent();
@ -165,7 +167,8 @@ describe("loop.standaloneRoomViews", function() {
it("should display a enjoy your conversation button if the state is ROOM_JOINED", function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.JOINED
roomState: ROOM_STATES.JOINED,
roomName: "fakeName"
});
view = mountTestComponent();
@ -176,7 +179,8 @@ describe("loop.standaloneRoomViews", function() {
it("should disable the enjoy your conversation button if the state is ROOM_JOINED", function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.JOINED
roomState: ROOM_STATES.JOINED,
roomName: "fakeName"
});
view = mountTestComponent();
@ -187,7 +191,8 @@ describe("loop.standaloneRoomViews", function() {
it("should not display a join button if there is a failure reason", function() {
activeRoomStore.setStoreState({
failureReason: FAILURE_DETAILS.ROOM_ALREADY_OPEN
failureReason: FAILURE_DETAILS.ROOM_ALREADY_OPEN,
roomName: "fakeName"
});
view = mountTestComponent();
@ -198,7 +203,8 @@ describe("loop.standaloneRoomViews", function() {
it("should display a room already joined message if opening failed", function() {
activeRoomStore.setStoreState({
failureReason: FAILURE_DETAILS.ROOM_ALREADY_OPEN
failureReason: FAILURE_DETAILS.ROOM_ALREADY_OPEN,
roomName: "fakeName"
});
view = mountTestComponent();
@ -206,6 +212,59 @@ describe("loop.standaloneRoomViews", function() {
expect(text.textContent).eql("rooms_already_joined");
});
describe("Room name priority", function() {
it("should use room name", function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.JOINED,
roomName: "fakeName"
});
view = mountTestComponent();
var text = view.getDOMNode().querySelector(".roomName");
expect(
text.textContent)
.eql("fakeName");
});
it("should use context title when there's no room title", function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.JOINED,
roomContextUrls: [
{
description: "Website title",
location: "https://fakeurl.com"
}
]
});
view = mountTestComponent();
var text = view.getDOMNode().querySelector(".roomName");
expect(
text.textContent)
.eql("Website title");
});
it("should use website url when there's no room title nor website", function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.JOINED,
roomContextUrls: [
{
location: "https://fakeurl.com"
}
]
});
view = mountTestComponent();
var text = view.getDOMNode().querySelector(".roomName");
expect(
text.textContent)
.eql("https://fakeurl.com");
});
});
});
describe("StandaloneRoomHeader", function() {
@ -241,7 +300,10 @@ describe("loop.standaloneRoomViews", function() {
}
beforeEach(function() {
activeRoomStore.setStoreState({ roomState: ROOM_STATES.FAILED });
activeRoomStore.setStoreState({
roomState: ROOM_STATES.FAILED,
roomName: "fakeName"
});
});
it("should display a status error message if not reason is supplied", function() {
@ -365,20 +427,15 @@ describe("loop.standaloneRoomViews", function() {
}
describe("#componentWillUpdate", function() {
it("should set document.title to roomName and brand name when the READY state is dispatched", function() {
activeRoomStore.setStoreState({ roomName: "fakeName", roomState: ROOM_STATES.INIT });
view = mountTestComponent();
activeRoomStore.setStoreState({ roomState: ROOM_STATES.READY });
expect(fakeWindow.document.title).to.equal("fakeName — clientShortname2");
beforeEach(function() {
activeRoomStore.setStoreState({ roomName: "fakeName" });
});
it("should set document.title brand name when there is no context available", function() {
it("should set document.title to roomName and brand name when the READY state is dispatched", function() {
activeRoomStore.setStoreState({ roomState: ROOM_STATES.INIT });
view = mountTestComponent();
activeRoomStore.setStoreState({ roomState: ROOM_STATES.READY });
expect(fakeWindow.document.title).to.equal("clientShortname2");
expect(fakeWindow.document.title).to.equal("fakeName — clientShortname2");
});
it("should dispatch a `SetupStreamElements` action when the MEDIA_WAIT state " +
@ -456,7 +513,7 @@ describe("loop.standaloneRoomViews", function() {
view = mountTestComponent();
// Pretend the user waited a little bit
activeRoomStore.setStoreState({ roomState: ROOM_STATES.JOINING });
activeRoomStore.setStoreState({ roomState: ROOM_STATES.JOINING, roomName: "fakeName" });
clock.tick(loop.standaloneRoomViews.StandaloneRoomInfoArea.RENDER_WAITING_DELAY - 1);
});
@ -541,7 +598,7 @@ describe("loop.standaloneRoomViews", function() {
describe("#render", function() {
beforeEach(function() {
view = mountTestComponent();
activeRoomStore.setStoreState({ roomState: ROOM_STATES.JOINING });
activeRoomStore.setStoreState({ roomState: ROOM_STATES.JOINING, roomName: "fakeName" });
});
describe("Empty room message", function() {
@ -976,6 +1033,10 @@ describe("loop.standaloneRoomViews", function() {
}));
}
beforeEach(function() {
activeRoomStore.setStoreState({ roomName: "fakeName" });
});
it("should not display anything if it is not known if Firefox can handle the room", function() {
activeRoomStore.setStoreState({
userAgentHandlesRoom: undefined

View File

@ -1332,8 +1332,63 @@
}
});
var Failure = React.createClass({displayName: "Failure",
propTypes: {
errorDetected: React.PropTypes.bool.isRequired,
errorLine1: React.PropTypes.string,
errorLine2: React.PropTypes.string,
summary: React.PropTypes.string.isRequired
},
render: function() {
// if no errors, return blank
return !this.props.errorDetected ? null :
(React.createElement("li", {className: "test fail"},
React.createElement("h2", null,
this.props.summary
),
React.createElement("pre", {className: "error"},
this.props.errorLine1 +
this.props.errorLine2 ? "\n" + this.props.errorLine2 : ""
)
)
);
}
});
var Result = React.createClass({displayName: "Result",
propTypes: {
error: React.PropTypes.object,
warnings: React.PropTypes.array
},
render: function() {
var warningsDetected = this.props.warnings.length !== 0;
var totalFailures = warningsDetected + !!this.props.error;
return (
React.createElement("div", {className: "error-summary"},
React.createElement("div", {className: "failures"},
React.createElement("a", null, "failures: "),
React.createElement("em", null, totalFailures)
),
React.createElement("ul", null,
React.createElement(Failure, {errorDetected: warningsDetected,
errorLine1: "Got: " + this.props.warnings.length,
summary: "Unexpected warnings detected rendering UI-Showcase"}),
React.createElement(Failure, {errorDetected: !!this.props.error,
errorLine1: this.props.error,
errorLine2: this.props.error ? this.props.error.stack : null,
summary: "Errors rendering UI-Showcase"})
),
React.createElement("p", {id: "complete"}, "Completed")
)
);
}
});
window.addEventListener("DOMContentLoaded", function() {
var uncaughtError;
var uncaughtError = null;
var consoleWarn = console.warn;
var caughtWarnings = [];
console.warn = function() {
@ -1362,54 +1417,9 @@
// Put the title back, in case views changed it.
document.title = "Loop UI Components Showcase";
// This simulates the mocha layout for errors which means we can run
// this alongside our other unit tests but use the same harness.
var expectedWarningsCount = 0;
var warningsMismatch = caughtWarnings.length !== expectedWarningsCount;
var resultsElement = document.querySelector("#results");
var divFailuresNode = document.createElement("div");
var pCompleteNode = document.createElement("p");
var emNode = document.createElement("em");
if (uncaughtError || warningsMismatch) {
var liTestFail = document.createElement("li");
var h2Node = document.createElement("h2");
var preErrorNode = document.createElement("pre");
divFailuresNode.className = "failures";
emNode.innerHTML = ((uncaughtError && warningsMismatch) ? 2 : 1).toString();
divFailuresNode.appendChild(emNode);
resultsElement.appendChild(divFailuresNode);
if (warningsMismatch) {
liTestFail.className = "test";
liTestFail.className += " fail";
h2Node.innerHTML = "Unexpected number of warnings detected in UI-Showcase";
preErrorNode.className = "error";
preErrorNode.innerHTML = "Got: " + caughtWarnings.length + "\n" + "Expected: " + expectedWarningsCount;
liTestFail.appendChild(h2Node);
liTestFail.appendChild(preErrorNode);
resultsElement.appendChild(liTestFail);
}
if (uncaughtError) {
liTestFail.className = "test";
liTestFail.className += " fail";
h2Node.innerHTML = "Errors rendering UI-Showcase";
preErrorNode.className = "error";
preErrorNode.innerHTML = uncaughtError + "\n" + uncaughtError.stack;
liTestFail.appendChild(h2Node);
liTestFail.appendChild(preErrorNode);
resultsElement.appendChild(liTestFail);
}
} else {
divFailuresNode.className = "failures";
emNode.innerHTML = "0";
divFailuresNode.appendChild(emNode);
resultsElement.appendChild(divFailuresNode);
}
pCompleteNode.id = "complete";
pCompleteNode.innerHTML = "Completed";
resultsElement.appendChild(pCompleteNode);
React.render(React.createElement(Result, {error: uncaughtError,
warnings: caughtWarnings}),
document.querySelector("#results"));
}, 1000);
});

View File

@ -1332,8 +1332,63 @@
}
});
var Failure = React.createClass({
propTypes: {
errorDetected: React.PropTypes.bool.isRequired,
errorLine1: React.PropTypes.string,
errorLine2: React.PropTypes.string,
summary: React.PropTypes.string.isRequired
},
render: function() {
// if no errors, return blank
return !this.props.errorDetected ? null :
(<li className = "test fail">
<h2>
{this.props.summary}
</h2>
<pre className="error">
{this.props.errorLine1 +
this.props.errorLine2 ? "\n" + this.props.errorLine2 : ""}
</pre>
</li>
);
}
});
var Result = React.createClass({
propTypes: {
error: React.PropTypes.object,
warnings: React.PropTypes.array
},
render: function() {
var warningsDetected = this.props.warnings.length !== 0;
var totalFailures = warningsDetected + !!this.props.error;
return (
<div className = "error-summary">
<div className = "failures">
<a>failures: </a>
<em>{totalFailures}</em>
</div>
<ul>
<Failure errorDetected={warningsDetected}
errorLine1={"Got: " + this.props.warnings.length}
summary="Unexpected warnings detected rendering UI-Showcase" />
<Failure errorDetected={!!this.props.error}
errorLine1={this.props.error}
errorLine2={this.props.error ? this.props.error.stack : null}
summary="Errors rendering UI-Showcase" />
</ul>
<p id="complete">Completed</p>
</div>
);
}
});
window.addEventListener("DOMContentLoaded", function() {
var uncaughtError;
var uncaughtError = null;
var consoleWarn = console.warn;
var caughtWarnings = [];
console.warn = function() {
@ -1362,54 +1417,9 @@
// Put the title back, in case views changed it.
document.title = "Loop UI Components Showcase";
// This simulates the mocha layout for errors which means we can run
// this alongside our other unit tests but use the same harness.
var expectedWarningsCount = 0;
var warningsMismatch = caughtWarnings.length !== expectedWarningsCount;
var resultsElement = document.querySelector("#results");
var divFailuresNode = document.createElement("div");
var pCompleteNode = document.createElement("p");
var emNode = document.createElement("em");
if (uncaughtError || warningsMismatch) {
var liTestFail = document.createElement("li");
var h2Node = document.createElement("h2");
var preErrorNode = document.createElement("pre");
divFailuresNode.className = "failures";
emNode.innerHTML = ((uncaughtError && warningsMismatch) ? 2 : 1).toString();
divFailuresNode.appendChild(emNode);
resultsElement.appendChild(divFailuresNode);
if (warningsMismatch) {
liTestFail.className = "test";
liTestFail.className += " fail";
h2Node.innerHTML = "Unexpected number of warnings detected in UI-Showcase";
preErrorNode.className = "error";
preErrorNode.innerHTML = "Got: " + caughtWarnings.length + "\n" + "Expected: " + expectedWarningsCount;
liTestFail.appendChild(h2Node);
liTestFail.appendChild(preErrorNode);
resultsElement.appendChild(liTestFail);
}
if (uncaughtError) {
liTestFail.className = "test";
liTestFail.className += " fail";
h2Node.innerHTML = "Errors rendering UI-Showcase";
preErrorNode.className = "error";
preErrorNode.innerHTML = uncaughtError + "\n" + uncaughtError.stack;
liTestFail.appendChild(h2Node);
liTestFail.appendChild(preErrorNode);
resultsElement.appendChild(liTestFail);
}
} else {
divFailuresNode.className = "failures";
emNode.innerHTML = "0";
divFailuresNode.appendChild(emNode);
resultsElement.appendChild(divFailuresNode);
}
pCompleteNode.id = "complete";
pCompleteNode.innerHTML = "Completed";
resultsElement.appendChild(pCompleteNode);
React.render(<Result error={uncaughtError}
warnings={caughtWarnings} />,
document.querySelector("#results"));
}, 1000);
});

View File

@ -56,6 +56,8 @@ const MAX_SUGGESTIONS = 6;
* data: the entry, a string
* Search
* Performs a search.
* Any GetSuggestions messages in the queue from the same target will be
* cancelled.
* data: { engineName, searchString, healthReportKey, searchPurpose }
* SetCurrentEngine
* Sets the current engine.
@ -81,6 +83,10 @@ const MAX_SUGGESTIONS = 6;
* Suggestions
* Sent in reply to GetSuggestions.
* data: see _onMessageGetSuggestions
* SuggestionsCancelled
* Sent in reply to GetSuggestions when pending GetSuggestions events are
* cancelled.
* data: null
*/
this.ContentSearch = {
@ -98,6 +104,10 @@ this.ContentSearch = {
// Resolved when we finish shutting down.
_destroyedPromise: null,
// The current controller and browser in _onMessageGetSuggestions. Allows
// fetch cancellation from _cancelSuggestions.
_currentSuggestion: null,
init: function () {
Cc["@mozilla.org/globalmessagemanager;1"].
getService(Ci.nsIMessageListenerManager).
@ -165,6 +175,12 @@ this.ContentSearch = {
};
msg.target.addEventListener("SwapDocShells", msg, true);
// Search requests cause cancellation of all Suggestion requests from the
// same browser.
if (msg.data.type == "Search") {
this._cancelSuggestions(msg);
}
this._eventQueue.push({
type: "Message",
data: msg,
@ -208,6 +224,27 @@ this.ContentSearch = {
}.bind(this));
},
_cancelSuggestions: function (msg) {
let cancelled = false;
// cancel active suggestion request
if (this._currentSuggestion && this._currentSuggestion.target == msg.target) {
this._currentSuggestion.controller.stop();
cancelled = true;
}
// cancel queued suggestion requests
for (let i = 0; i < this._eventQueue.length; i++) {
let m = this._eventQueue[i].data;
if (msg.target == m.target && m.data.type == "GetSuggestions") {
this._eventQueue.splice(i, 1);
cancelled = true;
i--;
}
}
if (cancelled) {
this._reply(msg, "SuggestionsCancelled");
}
},
_onMessage: Task.async(function* (msg) {
let methodName = "_onMessage" + msg.data.type;
if (methodName in this) {
@ -302,7 +339,14 @@ this.ContentSearch = {
let priv = PrivateBrowsingUtils.isBrowserPrivate(msg.target);
// fetch() rejects its promise if there's a pending request, but since we
// process our event queue serially, there's never a pending request.
this._currentSuggestion = { controller: controller, target: msg.target };
let suggestions = yield controller.fetch(data.searchString, priv, engine);
this._currentSuggestion = null;
// suggestions will be null if the request was cancelled
if (!suggestions) {
return;
}
// Keep the form history result so RemoveFormHistoryEntry can remove entries
// from it. Keeping only one result isn't foolproof because the client may

View File

@ -1947,6 +1947,7 @@ toolbarbutton.chevron > .toolbarbutton-icon {
.browser-extension-panel > .panel-arrowcontainer > .panel-arrowcontent {
padding: 0;
overflow: hidden;
}
%include ../shared/usercontext/usercontext.inc.css

View File

@ -3621,6 +3621,7 @@ notification[value="loop-sharing-notification"] .messageImage {
.browser-extension-panel > .panel-arrowcontainer > .panel-arrowcontent {
padding: 0;
overflow: hidden;
}
%include ../shared/usercontext/usercontext.inc.css

View File

@ -2807,6 +2807,7 @@ notification[value="loop-sharing-notification"] .messageImage {
.browser-extension-panel > .panel-arrowcontainer > .panel-arrowcontent {
padding: 0;
overflow: hidden;
}
%include ../shared/usercontext/usercontext.inc.css

View File

@ -337,6 +337,19 @@ fi
])
AC_DEFUN([MOZ_ANDROID_GOOGLE_CLOUD_MESSAGING],
[
if test -n "$MOZ_ANDROID_GCM" ; then
AC_SUBST(MOZ_ANDROID_GCM)
MOZ_ANDROID_AAR(play-services-base, 8.1.0, google, com/google/android/gms)
MOZ_ANDROID_AAR(play-services-basement, 8.1.0, google, com/google/android/gms)
MOZ_ANDROID_AAR(play-services-gcm, 8.1.0, google, com/google/android/gms)
fi
])
dnl Configure an Android SDK.
dnl Arg 1: target SDK version, like 22.
dnl Arg 2: build tools version, like 22.0.1.

View File

@ -48,6 +48,8 @@ toolkit/library
profile
services
startupcache
devtools/server
devtools/shared
browser/app
browser/base
browser/components
@ -60,7 +62,6 @@ browser/app
toolkit/components/jsdownloads
toolkit/content
toolkit/crashreporter
devtools/shared
toolkit/forgetaboutsite
toolkit/identity
toolkit/modules

View File

@ -3755,6 +3755,7 @@ MOZ_LOCALE_SWITCHER=
MOZ_ANDROID_READING_LIST_SERVICE=
MOZ_ANDROID_SEARCH_ACTIVITY=
MOZ_ANDROID_DOWNLOADS_INTEGRATION=
MOZ_ANDROID_GCM=
MOZ_ANDROID_MLS_STUMBLER=
MOZ_ANDROID_SHARE_OVERLAY=
MOZ_EXCLUDE_HYPHENATION_DICTIONARIES=
@ -3911,6 +3912,14 @@ MOZ_ARG_WITH_STRING(adjust-sdk-keyfile,
MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN=`cat $withval`)
AC_SUBST(MOZ_INSTALL_TRACKING_ADJUST_SDK_APP_TOKEN)
# Allow specifying a GCM sender ID key file that contains the sender ID used for
# GCM requests. Note that GCM sender IDs are not sensitive: see, for example,
# http://stackoverflow.com/a/18216063.
MOZ_ARG_WITH_STRING(gcm-senderid-keyfile,
[ --with-gcm-senderid-keyfile=file GCM sender ID for GCM requests],
MOZ_ANDROID_GCM_SENDERID=`cat $withval`)
AC_SUBST(MOZ_ANDROID_GCM_SENDERID)
# Whether to include optional-but-large font files in the final APK.
# We want this in mobile/android/confvars.sh, so it goes early.
MOZ_ARG_DISABLE_BOOL(android-include-fonts,
@ -4580,6 +4589,7 @@ dnl values set by configure.sh above.
dnl ========================================================
MOZ_ANDROID_GOOGLE_PLAY_SERVICES
MOZ_ANDROID_GOOGLE_CLOUD_MESSAGING
dnl ========================================================
@ -4886,6 +4896,13 @@ if test -n "$MOZ_SWITCHBOARD"; then
AC_DEFINE(MOZ_SWITCHBOARD)
fi
dnl ========================================================
dnl = Enable GCM on Android.
dnl ========================================================
if test -n "$MOZ_ANDROID_GCM"; then
AC_DEFINE(MOZ_ANDROID_GCM)
fi
dnl ========================================================
dnl = Enable IPDL's "expensive" unit tests
dnl ========================================================
@ -8592,6 +8609,7 @@ AC_SUBST(MOZ_WEBSMS_BACKEND)
AC_SUBST(MOZ_ANDROID_BEAM)
AC_SUBST(MOZ_LOCALE_SWITCHER)
AC_SUBST(MOZ_DISABLE_GECKOVIEW)
AC_SUBST(MOZ_ANDROID_GCM)
AC_SUBST(MOZ_ANDROID_GECKOLIBS_AAR)
AC_SUBST(MOZ_ANDROID_READING_LIST_SERVICE)
AC_SUBST(MOZ_ANDROID_SEARCH_ACTIVITY)

View File

@ -25,8 +25,15 @@ function test() {
}
function performTest() {
// Make sure that the search box becomes focused when pressing ctrl+f - Bug 1211038
gEditor.focus();
synthesizeKeyFromKeyTag(gDebugger.document.getElementById("tokenSearchKey"));
let focusedEl = Services.focus.focusedElement;
focusedEl = focusedEl.ownerDocument.getBindingParent(focusedEl) || focusedEl;
is(focusedEl, gDebugger.document.getElementById("searchbox"), "Searchbox is focused");
setText(gSearchBox, "#html");
EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true }, gDebugger);
is(gFiltering.searchData.toSource(), '["#", ["", "html"]]',
"The searchbox data wasn't parsed correctly.");
@ -44,7 +51,7 @@ function performTest() {
"The searchbox data wasn't parsed correctly.");
ok(isCaretPos(gPanel, 3, 15),
"The editor didn't jump to the correct line.");
setText(gSearchBox, ":12");
is(gFiltering.searchData.toSource(), '[":", ["", 12]]',
"The searchbox data wasn't parsed correctly.");

View File

@ -1215,3 +1215,30 @@ function getSplitConsole(toolbox, win) {
});
});
}
// This can be removed once debugger uses shared-head.js (bug 1181838)
function synthesizeKeyFromKeyTag(key) {
is(key && key.tagName, "key", "Successfully retrieved the <key> node");
let modifiersAttr = key.getAttribute("modifiers");
let name = null;
if (key.getAttribute("keycode"))
name = key.getAttribute("keycode");
else if (key.getAttribute("key"))
name = key.getAttribute("key");
isnot(name, null, "Successfully retrieved keycode/key");
let modifiers = {
shiftKey: !!modifiersAttr.match("shift"),
ctrlKey: !!modifiersAttr.match("control"),
altKey: !!modifiersAttr.match("alt"),
metaKey: !!modifiersAttr.match("meta"),
accelKey: !!modifiersAttr.match("accel")
};
info("Synthesizing key " + name + " " + JSON.stringify(modifiers));
EventUtils.synthesizeKey(name, modifiers);
}

View File

@ -3,25 +3,16 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// @TODO 1215606
// Use this assert instead of utils when fixed.
// const { assert } = require("devtools/shared/DevToolsUtils");
const { breakdownEquals, createSnapshot, assert } = require("../utils");
const { assert } = require("devtools/shared/DevToolsUtils");
const { breakdownEquals, createSnapshot } = require("../utils");
const { actions, snapshotState: states } = require("../constants");
const { takeCensus } = require("./snapshot");
const { refreshSelectedCensus } = require("./snapshot");
const setBreakdownAndRefresh = exports.setBreakdownAndRefresh = function (heapWorker, breakdown) {
return function *(dispatch, getState) {
// Clears out all stored census data and sets
// the breakdown
// Clears out all stored census data and sets the breakdown.
dispatch(setBreakdown(breakdown));
let snapshot = getState().snapshots.find(s => s.selected);
// If selected snapshot does not have updated census if the breakdown
// changed, retake the census with new breakdown
if (snapshot && !breakdownEquals(snapshot.breakdown, breakdown)) {
yield dispatch(takeCensus(heapWorker, snapshot));
}
yield dispatch(refreshSelectedCensus(heapWorker));
};
};
@ -32,7 +23,6 @@ const setBreakdownAndRefresh = exports.setBreakdownAndRefresh = function (heapWo
* @param {Breakdown} breakdown
*/
const setBreakdown = exports.setBreakdown = function (breakdown) {
// @TODO 1215606
assert(typeof breakdown === "object" && breakdown.by,
`Breakdowns must be an object with a \`by\` property, attempted to set: ${uneval(breakdown)}`);

View File

@ -0,0 +1,18 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { actions } = require("../constants");
const { refreshSelectedCensus } = require("./snapshot");
const toggleInverted = exports.toggleInverted = function () {
return { type: actions.TOGGLE_INVERTED };
};
exports.toggleInvertedAndRefresh = function (heapWorker) {
return function* (dispatch, getState) {
dispatch(toggleInverted());
yield dispatch(refreshSelectedCensus(heapWorker));
};
};

View File

@ -6,5 +6,6 @@
DevToolsModules(
'allocations.js',
'breakdown.js',
'inverted.js',
'snapshot.js',
)

View File

@ -3,10 +3,8 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
// @TODO 1215606
// Use this assert instead of utils when fixed.
// const { assert } = require("devtools/shared/DevToolsUtils");
const { getSnapshot, breakdownEquals, createSnapshot, assert } = require("../utils");
const { assert, reportException } = require("devtools/shared/DevToolsUtils");
const { getSnapshot, breakdownEquals, createSnapshot } = require("../utils");
const { actions, snapshotState: states } = require("../constants");
/**
@ -20,8 +18,11 @@ const { actions, snapshotState: states } = require("../constants");
const takeSnapshotAndCensus = exports.takeSnapshotAndCensus = function (front, heapWorker) {
return function *(dispatch, getState) {
let snapshot = yield dispatch(takeSnapshot(front));
yield dispatch(readSnapshot(heapWorker, snapshot));
yield dispatch(takeCensus(heapWorker, snapshot));
if (snapshot.state === states.READ) {
yield dispatch(takeCensus(heapWorker, snapshot));
}
};
};
@ -35,10 +36,7 @@ const takeSnapshotAndCensus = exports.takeSnapshotAndCensus = function (front, h
const selectSnapshotAndRefresh = exports.selectSnapshotAndRefresh = function (heapWorker, snapshot) {
return function *(dispatch, getState) {
dispatch(selectSnapshot(snapshot));
// Attempt to take another census; if the snapshot already is using
// the correct breakdown, this will noop.
yield dispatch(takeCensus(heapWorker, snapshot));
yield dispatch(refreshSelectedCensus(heapWorker));
};
};
@ -51,9 +49,16 @@ const takeSnapshot = exports.takeSnapshot = function (front) {
dispatch({ type: actions.TAKE_SNAPSHOT_START, snapshot });
dispatch(selectSnapshot(snapshot));
let path = yield front.saveHeapSnapshot();
dispatch({ type: actions.TAKE_SNAPSHOT_END, snapshot, path });
let path;
try {
path = yield front.saveHeapSnapshot();
} catch (error) {
reportException("takeSnapshot", error);
dispatch({ type: actions.SNAPSHOT_ERROR, snapshot, error });
return;
}
dispatch({ type: actions.TAKE_SNAPSHOT_END, snapshot, path });
return snapshot;
};
};
@ -67,12 +72,18 @@ const takeSnapshot = exports.takeSnapshot = function (front) {
*/
const readSnapshot = exports.readSnapshot = function readSnapshot (heapWorker, snapshot) {
return function *(dispatch, getState) {
// @TODO 1215606
assert(snapshot.state === states.SAVED,
"Should only read a snapshot once");
`Should only read a snapshot once. Found snapshot in state ${snapshot.state}`);
dispatch({ type: actions.READ_SNAPSHOT_START, snapshot });
yield heapWorker.readHeapSnapshot(snapshot.path);
try {
yield heapWorker.readHeapSnapshot(snapshot.path);
} catch (error) {
reportException("readSnapshot", error);
dispatch({ type: actions.SNAPSHOT_ERROR, snapshot, error });
return;
}
dispatch({ type: actions.READ_SNAPSHOT_END, snapshot });
};
};
@ -87,15 +98,15 @@ const readSnapshot = exports.readSnapshot = function readSnapshot (heapWorker, s
*/
const takeCensus = exports.takeCensus = function (heapWorker, snapshot) {
return function *(dispatch, getState) {
// @TODO 1215606
assert([states.READ, states.SAVED_CENSUS].includes(snapshot.state),
"Can only take census of snapshots in READ or SAVED_CENSUS state");
`Can only take census of snapshots in READ or SAVED_CENSUS state, found ${snapshot.state}`);
let census;
let inverted = getState().inverted;
let breakdown = getState().breakdown;
// If breakdown hasn't changed, don't do anything
if (breakdownEquals(breakdown, snapshot.breakdown)) {
// If breakdown and inversion haven't changed, don't do anything.
if (inverted === snapshot.inverted && breakdownEquals(breakdown, snapshot.breakdown)) {
return;
}
@ -103,12 +114,45 @@ const takeCensus = exports.takeCensus = function (heapWorker, snapshot) {
// that the breakdown used for the census is the same as
// the state's breakdown.
do {
inverted = getState().inverted;
breakdown = getState().breakdown;
dispatch({ type: actions.TAKE_CENSUS_START, snapshot, breakdown });
census = yield heapWorker.takeCensus(snapshot.path, { breakdown }, { asTreeNode: true });
} while (!breakdownEquals(breakdown, getState().breakdown));
dispatch({ type: actions.TAKE_CENSUS_START, snapshot, inverted, breakdown });
let opts = inverted ? { asInvertedTreeNode: true } : { asTreeNode: true };
dispatch({ type: actions.TAKE_CENSUS_END, snapshot, breakdown, census });
try {
census = yield heapWorker.takeCensus(snapshot.path, { breakdown }, opts);
} catch(error) {
reportException("takeCensus", error);
dispatch({ type: actions.SNAPSHOT_ERROR, snapshot, error });
return;
}
}
while (inverted !== getState().inverted ||
!breakdownEquals(breakdown, getState().breakdown));
dispatch({ type: actions.TAKE_CENSUS_END, snapshot, breakdown, inverted, census });
};
};
/**
* Refresh the selected snapshot's census data, if need be (for example,
* breakdown configuration changed).
*
* @param {HeapAnalysesClient} heapWorker
*/
const refreshSelectedCensus = exports.refreshSelectedCensus = function (heapWorker) {
return function*(dispatch, getState) {
let snapshot = getState().snapshots.find(s => s.selected);
// Intermediate snapshot states will get handled by the task action that is
// orchestrating them. For example, if the snapshot's state is
// SAVING_CENSUS, then the takeCensus action will keep taking a census until
// the inverted property matches the inverted state. If the snapshot is
// still in the process of being saved or read, the takeSnapshotAndCensus
// task action will follow through and ensure that a census is taken.
if (snapshot && snapshot.state === states.SAVED_CENSUS) {
yield dispatch(takeCensus(heapWorker, snapshot));
}
};
};

View File

@ -6,6 +6,7 @@ const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/cl
const { connect } = require("devtools/client/shared/vendor/react-redux");
const { toggleRecordingAllocationStacks } = require("./actions/allocations");
const { setBreakdownAndRefresh } = require("./actions/breakdown");
const { toggleInvertedAndRefresh } = require("./actions/inverted");
const { selectSnapshotAndRefresh, takeSnapshotAndCensus } = require("./actions/snapshot");
const { breakdownNameToSpec, getBreakdownDisplayData } = require("./utils");
const Toolbar = createFactory(require("./components/toolbar"));
@ -39,6 +40,7 @@ const App = createClass({
heapWorker,
breakdown,
allocations,
inverted
} = this.props;
let selectedSnapshot = snapshots.find(s => s.selected);
@ -53,7 +55,10 @@ const App = createClass({
dispatch(setBreakdownAndRefresh(heapWorker, breakdownNameToSpec(breakdown))),
onToggleRecordAllocationStacks: () =>
dispatch(toggleRecordingAllocationStacks(front)),
allocations
allocations,
inverted,
onToggleInverted: () =>
dispatch(toggleInvertedAndRefresh(heapWorker))
}),
dom.div({ id: "memory-tool-container" }, [

View File

@ -3,9 +3,10 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
const { DOM: dom, createClass, PropTypes, createFactory } = require("devtools/client/shared/vendor/react");
const { safeErrorString } = require("devtools/shared/DevToolsUtils");
const Tree = createFactory(require("./tree"));
const TreeItem = createFactory(require("./tree-item"));
const { getSnapshotStatusText } = require("../utils");
const { getSnapshotStatusTextFull } = require("../utils");
const { snapshotState: states } = require("../constants");
const { snapshot: snapshotModel } = require("../models");
const TAKE_SNAPSHOT_TEXT = "Take snapshot";
@ -40,8 +41,6 @@ function createTreeProperties (census) {
let map = createParentMap(census);
return {
// getParent only used for focusing parents when child selected;
// handle this later?
getParent: node => map(node.id),
getChildren: node => node.children || [],
renderItem: (item, depth, focused, arrow) => new TreeItem({ item, depth, focused, arrow }),
@ -67,26 +66,37 @@ const Heap = module.exports = createClass({
render() {
let { snapshot, onSnapshotClick } = this.props;
let pane;
let census = snapshot ? snapshot.census : null;
let state = snapshot ? snapshot.state : "initial";
let statusText = snapshot ? getSnapshotStatusTextFull(snapshot) : "";
let content;
switch (state) {
case "initial":
pane = dom.div({ className: "heap-view-panel", "data-state": "initial" },
dom.button({ className: "take-snapshot", onClick: onSnapshotClick }, TAKE_SNAPSHOT_TEXT)
);
content = dom.button({
className: "devtools-toolbarbutton take-snapshot",
onClick: onSnapshotClick,
// Want to use the [standalone] tag to leverage our styles,
// but React hates that evidently
"data-standalone": true,
"data-text-only": true,
}, TAKE_SNAPSHOT_TEXT)
break;
case states.ERROR:
content = [
dom.span({ className: "snapshot-status error" }, statusText),
dom.pre({}, safeErrorString(snapshot.error || new Error("blahblah"))),
];
break;
case states.SAVING:
case states.SAVED:
case states.READING:
case states.READ:
case states.SAVING_CENSUS:
pane = dom.div({ className: "heap-view-panel", "data-state": state },
getSnapshotStatusText(snapshot));
content = dom.span({ className: "snapshot-status devtools-throbber" }, statusText)
break;
case states.SAVED_CENSUS:
pane = dom.div({ className: "heap-view-panel", "data-state": "loaded" },
content = [
dom.div({ className: "header" },
dom.span({ className: "heap-tree-item-bytes" }, "Bytes"),
dom.span({ className: "heap-tree-item-count" }, "Count"),
@ -95,9 +105,10 @@ const Heap = module.exports = createClass({
dom.span({ className: "heap-tree-item-name" }, "Name")
),
Tree(createTreeProperties(snapshot.census))
);
];
break;
}
let pane = dom.div({ className: "heap-view-panel", "data-state": state }, content);
return (
dom.div({ id: "heap-view", "data-state": state }, pane)

View File

@ -16,7 +16,9 @@ const Toolbar = module.exports = createClass({
onTakeSnapshotClick: PropTypes.func.isRequired,
onBreakdownChange: PropTypes.func.isRequired,
onToggleRecordAllocationStacks: PropTypes.func.isRequired,
allocations: models.allocations
allocations: models.allocations,
onToggleInverted: PropTypes.func.isRequired,
inverted: PropTypes.bool.isRequired,
},
render() {
@ -26,6 +28,8 @@ const Toolbar = module.exports = createClass({
breakdowns,
onToggleRecordAllocationStacks,
allocations,
onToggleInverted,
inverted
} = this.props;
return (
@ -37,6 +41,16 @@ const Toolbar = module.exports = createClass({
onChange: e => onBreakdownChange(e.target.value),
}, breakdowns.map(({ name, displayName }) => DOM.option({ value: name }, displayName))),
DOM.label({}, [
DOM.input({
type: "checkbox",
checked: inverted,
onChange: onToggleInverted,
}),
// TODO bug 1214799
"Invert tree"
]),
DOM.label({}, [
DOM.input({
type: "checkbox",
@ -47,7 +61,7 @@ const Toolbar = module.exports = createClass({
// TODO bug 1214799
"Record allocation stacks"
])
])
])
);
}
});

View File

@ -2,8 +2,12 @@
* 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/. */
const { isSavedFrame } = require("devtools/shared/DevToolsUtils");
const { DOM: dom, createClass, PropTypes } = require("devtools/client/shared/vendor/react");
const INDENT = 10;
const MAX_SOURCE_LENGTH = 200;
/**
* An arrow that displays whether its node is expanded () or collapsed
@ -15,15 +19,34 @@ const TreeItem = module.exports = createClass({
render() {
let { item, depth, arrow, focused } = this.props;
return dom.div({ className: "heap-tree-item" },
return dom.div({ className: `heap-tree-item ${focused ? "focused" :""}` },
dom.span({ className: "heap-tree-item-bytes" }, item.bytes),
dom.span({ className: "heap-tree-item-count" }, item.count),
dom.span({ className: "heap-tree-item-total-bytes" }, item.totalBytes),
dom.span({ className: "heap-tree-item-total-count" }, item.totalCount),
dom.span({ className: "heap-tree-item-name", style: { marginLeft: depth * INDENT }},
arrow,
item.name
this.toLabel(item.name)
)
);
},
toLabel(name) {
return isSavedFrame(name)
? this.savedFrameToLabel(name)
: String(name);
},
savedFrameToLabel(frame) {
return [
dom.span({ className: "heap-tree-item-function-display-name" },
frame.functionDisplayFrame || ""),
dom.span({ className: "heap-tree-item-at" }, "@"),
dom.span({ className: "heap-tree-item-source" }, frame.source.slice(0, MAX_SOURCE_LENGTH)),
dom.span({ className: "heap-tree-item-colon" }, ":"),
dom.span({ className: "heap-tree-item-line" }, frame.line),
dom.span({ className: "heap-tree-item-colon" }, ":"),
dom.span({ className: "heap-tree-item-column" }, frame.column)
];
}
});

View File

@ -26,6 +26,12 @@ actions.TOGGLE_RECORD_ALLOCATION_STACKS_END = "toggle-record-allocation-stacks-e
// Fired by UI to select a snapshot to view.
actions.SELECT_SNAPSHOT = "select-snapshot";
// Fired to toggle tree inversion on or off.
actions.TOGGLE_INVERTED = "toggle-inverted";
// Fired when there is an error processing a snapshot or taking a census.
actions.SNAPSHOT_ERROR = "snapshot-error";
// Options passed to MemoryFront's startRecordingAllocations never change.
exports.ALLOCATION_RECORDING_OPTIONS = {
probability: 1,
@ -71,10 +77,14 @@ const snapshotState = exports.snapshotState = {};
* Various states a snapshot can be in.
* An FSM describing snapshot states:
*
* SAVING -> SAVED -> READING -> READ <- <- <- SAVED_CENSUS
*
* SAVING_CENSUS
* SAVING -> SAVED -> READING -> READ <- <- <- SAVED_CENSUS
*
* SAVING_CENSUS
*
* Any of these states may go to the ERROR state, from which they can never
* leave (mwah ha ha ha!)
*/
snapshotState.ERROR = "snapshot-state-error";
snapshotState.SAVING = "snapshot-state-saving";
snapshotState.SAVED = "snapshot-state-saved";
snapshotState.READING = "snapshot-state-reading";

View File

@ -31,6 +31,10 @@ let snapshotModel = exports.snapshot = PropTypes.shape({
census: PropTypes.object,
// The breakdown used to generate the current census
breakdown: breakdownModel,
// Whether the currently cached census tree is inverted or not.
inverted: PropTypes.bool,
// If an error was thrown while processing this snapshot, the `Error` instance is attached here.
error: PropTypes.object,
// State the snapshot is in
// @see ./constants.js
state: function (props, propName) {
@ -63,7 +67,7 @@ let appModel = exports.app = {
// {MemoryFront} Used to communicate with platform
front: PropTypes.instanceOf(MemoryFront),
// Allocations recording related data.
allocations: allocationsModel,
allocations: allocationsModel.isRequired,
// {HeapAnalysesClient} Used to interface with snapshots
heapWorker: PropTypes.instanceOf(HeapAnalysesClient),
// The breakdown object DSL describing how we want
@ -72,4 +76,6 @@ let appModel = exports.app = {
breakdown: breakdownModel.isRequired,
// List of reference to all snapshots taken
snapshots: PropTypes.arrayOf(snapshotModel).isRequired,
// True iff we want the tree displayed inverted.
inverted: PropTypes.bool.isRequired,
};

View File

@ -1,126 +0,0 @@
/* 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";
/**
* This file contains the tree view, displaying all the samples and frames
* received from the proviler in a tree-like structure.
*/
const { Cc, Ci, Cu, Cr } = require("chrome");
const { Heritage } = require("resource://devtools/client/shared/widgets/ViewHelpers.jsm");
const { AbstractTreeItem } = require("resource://devtools/client/shared/widgets/AbstractTreeItem.jsm");
const INDENTATION = exports.INDENTATION = 16; // px
const DEFAULT_AUTO_EXPAND_DEPTH = 2;
const COURSE_TYPES = ["objects", "scripts", "strings", "other"];
/**
* Every instance of a `CensusView` represents a row in the census tree. The same
* parent node is used for all rows.
*
* @param CensusView parent
* The CensusView considered the parent row. Should be null
* for root node.
* @param {CensusTreeNode} censusTreeNode
* Data from `takeCensus` transformed via `CensusTreeNode`.
* @see browser/toolkit/heapsnapshot/census-tree-node.js
* @param number level [optional]
* The indentation level in the call tree. The root node is at level 0.
* @param boolean hidden [optional]
* Whether this node should be hidden and not contribute to depth/level
* calculations. Defaults to false.
* @param number autoExpandDepth [optional]
* The depth to which the tree should automatically expand. Defaults to
* the caller's `autoExpandDepth` if a caller exists, otherwise defaults
* to DEFAULT_AUTO_EXPAND_DEPTH.
*/
function CensusView ({ caller, censusTreeNode, level, hidden, autoExpandDepth }) {
AbstractTreeItem.call(this, { parent: caller, level: level|0 - (hidden ? 1 : 0) });
this.autoExpandDepth = autoExpandDepth != null
? autoExpandDepth
: caller ? caller.autoExpandDepth
: DEFAULT_AUTO_EXPAND_DEPTH;
this.caller = caller;
this.censusTreeNode = censusTreeNode;
this.hidden = hidden;
};
CensusView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
/**
* Creates the view for this tree node.
* @param nsIDOMNode document
* @param nsIDOMNode arrowNode
* @return nsIDOMNode
*/
_displaySelf: function (document, arrowNode) {
let data = this.censusTreeNode;
let cells = [];
// Only use an arrow if there are children
if (data.children && data.children.length) {
cells.push(arrowNode);
}
cells.push(this._createCell(document, data.name, "name"));
if (data.bytes != null) {
cells.push(this._createCell(document, data.bytes, "bytes"));
}
if (data.count != null) {
cells.push(this._createCell(document, data.count, "count"));
}
let targetNode = document.createElement("li");
targetNode.className = "heap-tree-item";
targetNode.style.MozMarginStart = `${this.level * INDENTATION}px`;
if (this.hidden) {
targetNode.style.display = "none";
}
for (let i = 0; i < cells.length; i++) {
targetNode.appendChild(cells[i]);
}
return targetNode;
},
/**
* Populates this node in the call tree with the corresponding "callees".
* These are defined in the `frame` data source for this call view.
* @param array:AbstractTreeItem children
*/
_populateSelf: function (children) {
let newLevel = this.level + 1;
let data = this.censusTreeNode;
if (data.children) {
for (let node of data.children) {
children.push(new CensusView({
caller: this,
level: newLevel,
censusTreeNode: node,
}));
}
}
},
/**
* Functions creating each cell in this call view.
* Invoked by `_displaySelf`.
*/
_createCell: function (doc, value, type) {
let cell = doc.createElement("span");
cell.className = "plain heap-tree-cell";
cell.setAttribute("type", type);
cell.innerHTML = value;
return cell;
},
});
exports.CensusView = CensusView;

View File

@ -1,8 +0,0 @@
# vim: set filetype=python:
# 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/.
DevToolsModules(
'census-view.js',
)

View File

@ -6,7 +6,6 @@
DIRS += [
'actions',
'components',
'modules',
'reducers',
]
@ -22,5 +21,4 @@ DevToolsModules(
)
BROWSER_CHROME_MANIFESTS += ['test/browser/browser.ini']
MOCHITEST_CHROME_MANIFESTS += ['test/mochitest/chrome.ini']
XPCSHELL_TESTS_MANIFESTS += ['test/unit/xpcshell.ini']

View File

@ -7,3 +7,4 @@ exports.allocations = require("./reducers/allocations");
exports.snapshots = require("./reducers/snapshots");
exports.breakdown = require("./reducers/breakdown");
exports.errors = require("./reducers/errors");
exports.inverted = require("./reducers/inverted");

View File

@ -0,0 +1,10 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { actions } = require("../constants");
module.exports = function (inverted = false, action) {
return action.type === actions.TOGGLE_INVERTED ? !inverted : inverted;
};

View File

@ -7,5 +7,6 @@ DevToolsModules(
'allocations.js',
'breakdown.js',
'errors.js',
'inverted.js',
'snapshots.js',
)

View File

@ -7,6 +7,13 @@ const { getSnapshot } = require("../utils");
let handlers = Object.create({});
handlers[actions.SNAPSHOT_ERROR] = function (snapshots, action) {
let snapshot = getSnapshot(snapshots, action.snapshot);
snapshot.state = states.ERROR;
snapshot.error = action.error;
return [...snapshots];
};
handlers[actions.TAKE_SNAPSHOT_START] = function (snapshots, { snapshot }) {
return [...snapshots, snapshot];
};
@ -38,6 +45,7 @@ handlers[actions.TAKE_CENSUS_START] = function (snapshots, action) {
snapshot.state = states.SAVING_CENSUS;
snapshot.census = null;
snapshot.breakdown = action.breakdown;
snapshot.inverted = action.inverted;
return [...snapshots];
};
@ -46,6 +54,7 @@ handlers[actions.TAKE_CENSUS_END] = function (snapshots, action) {
snapshot.state = states.SAVED_CENSUS;
snapshot.census = action.census;
snapshot.breakdown = action.breakdown;
snapshot.inverted = action.inverted;
return [...snapshots];
};

View File

@ -1,7 +0,0 @@
[DEFAULT]
tags = devtools
skip-if = buildapp == 'b2g' || os == 'android'
support-files =
head.js
[test_census-view-01.html]

View File

@ -1,12 +0,0 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
var Cc = Components.classes;
var Ci = Components.interfaces;
var Cu = Components.utils;
var Cr = Components.results;
var CC = Components.Constructor;
const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});

View File

@ -1,103 +0,0 @@
<!DOCTYPE HTML>
<html>
<!--
Bug 1067491 - Test taking a census over the RDP.
-->
<head>
<meta charset="utf-8">
<title>Census Tree 01</title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<link href="chrome://devtools/content/shared/widgets/widgets.css" type="text/css" />
<link href="chrome://devtools/skin/themes/light-theme.css" type="text/css" />
<link href="chrome://devtools/skin/themes/common.css" type="text/css" />
<link href="chrome://devtools/skin/themes/widgets.css" type="text/css" />
<link href="chrome://devtools/skin/themes/memory.css" type="text/css" />
</head>
<body>
<ul id="container" style="width:100%;height:300px;"></ul>
<pre id="test">
<script src="head.js" type="application/javascript;version=1.8"></script>
<script>
window.onload = function() {
var { censusReportToCensusTreeNode } = require("devtools/shared/heapsnapshot/census-tree-node");
var { INDENTATION, CensusView } = require("devtools/client/memory/modules/census-view");
SimpleTest.waitForExplicitFinish();
const countBreakdown = { by: "count", count: true, bytes: true };
const BREAKDOWN = {
by: "coarseType",
objects: { by: "objectClass", then: countBreakdown },
strings: countBreakdown,
scripts: countBreakdown,
other: { by: "internalType", then: countBreakdown },
};
const REPORT = {
"objects": {
"Function": { bytes: 10, count: 1 },
"Array": { bytes: 20, count: 2 },
},
"strings": { bytes: 10, count: 1 },
"scripts": { bytes: 20, count: 2 },
"other": {
"js::Shape": { bytes: 30, count: 3 },
"js::Shape2": { bytes: 40, count: 4 }
},
};
const EXPECTED_ROWS = [
{ level: 0, name: "other", bytes: 0, count: 0 },
{ level: 1, name: "js::Shape2", bytes: 40, count: 4, },
{ level: 1, name: "js::Shape", bytes: 30, count: 3, },
{ level: 0, name: "objects", bytes: 0, count: 0 },
{ level: 1, name: "Array", bytes: 20, count: 2, },
{ level: 1, name: "Function", bytes: 10, count: 1, },
{ level: 0, name: "scripts", bytes: 20, count: 2, },
{ level: 0, name: "strings", bytes: 10, count: 1, },
];
var censusTreeNode = censusReportToCensusTreeNode(BREAKDOWN, REPORT);
var view = new CensusView({
censusTreeNode: censusTreeNode,
hidden: true
});
view.attachTo(document.querySelector("#container"));
var ul = document.querySelector("#container");
var children = Array.from(ul.children).filter(n => n.style.display !== "none");
for (var i = 0; i < children.length; i++) {
var el = children[i];
var expected = EXPECTED_ROWS[i];
var nameEl = el.querySelector(".heap-tree-cell[type='name']");
var bytesEl = el.querySelector(".heap-tree-cell[type='bytes']");
var countEl = el.querySelector(".heap-tree-cell[type='count']");
is(nameEl.innerHTML, expected.name,
`correct name "${expected.name}" in heap tree`);
is(el.style.MozMarginStart, (INDENTATION * expected.level) + "px",
`correct indentation for ${expected.name}`);
if ("bytes" in expected) {
is(bytesEl.innerHTML, String(expected.bytes),
`correct bytes "${expected.bytes}" in heap tree`);
} else {
ok(!bytesEl, `no bytes correctly displayed for ${expected.name}`);
}
if ("count" in expected) {
is(countEl.innerHTML, String(expected.count),
`correct count "${expected.count}" in heap tree`);
} else {
ok(!countEl, `no count correctly displayed for ${expected.name}`);
}
}
SimpleTest.finish();
};
</script>
</pre>
</body>
</html>

View File

@ -0,0 +1,64 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that changing inverted state properly refreshes the selected census.
let { breakdowns, snapshotState: states } = require("devtools/client/memory/constants");
let { toggleInvertedAndRefresh } = require("devtools/client/memory/actions/inverted");
let { takeSnapshotAndCensus, selectSnapshotAndRefresh } = require("devtools/client/memory/actions/snapshot");
function run_test() {
run_next_test();
}
add_task(function *() {
let front = new StubbedMemoryFront();
let heapWorker = new HeapAnalysesClient();
yield front.attach();
let store = Store();
let { getState, dispatch } = store;
equal(getState().inverted, false, "not inverted by default");
dispatch(takeSnapshotAndCensus(front, heapWorker));
dispatch(takeSnapshotAndCensus(front, heapWorker));
dispatch(takeSnapshotAndCensus(front, heapWorker));
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
states.SAVED_CENSUS,
states.SAVED_CENSUS]);
ok(true, "saved 3 snapshots and took a census of each of them");
dispatch(toggleInvertedAndRefresh(heapWorker));
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
states.SAVED_CENSUS,
states.SAVING_CENSUS]);
ok(true, "toggling inverted should recompute the selected snapshot's census");
equal(getState().inverted, true, "now inverted");
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
states.SAVED_CENSUS,
states.SAVED_CENSUS]);
equal(getState().snapshots[0].inverted, false);
equal(getState().snapshots[1].inverted, false);
equal(getState().snapshots[2].inverted, true);
dispatch(selectSnapshotAndRefresh(heapWorker, getState().snapshots[1]));
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
states.SAVING_CENSUS,
states.SAVED_CENSUS]);
ok(true, "selecting non-inverted census should trigger a recompute");
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS,
states.SAVED_CENSUS,
states.SAVED_CENSUS]);
equal(getState().snapshots[0].inverted, false);
equal(getState().snapshots[1].inverted, true);
equal(getState().snapshots[2].inverted, true);
heapWorker.destroy();
yield front.detach();
});

View File

@ -0,0 +1,46 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that changing inverted state in the middle of taking a snapshot results
// in an inverted census.
let { snapshotState: states } = require("devtools/client/memory/constants");
let { toggleInverted, toggleInvertedAndRefresh } = require("devtools/client/memory/actions/inverted");
let { takeSnapshotAndCensus, selectSnapshotAndRefresh } = require("devtools/client/memory/actions/snapshot");
function run_test() {
run_next_test();
}
add_task(function *() {
let front = new StubbedMemoryFront();
let heapWorker = new HeapAnalysesClient();
yield front.attach();
let store = Store();
let { getState, dispatch } = store;
dispatch(takeSnapshotAndCensus(front, heapWorker));
yield waitUntilSnapshotState(store, [states.SAVING]);
dispatch(toggleInverted());
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
ok(getState().inverted,
"should want inverted trees");
ok(getState().snapshots[0].inverted,
"snapshot-we-were-in-the-middle-of-saving's census should be inverted");
dispatch(toggleInvertedAndRefresh(heapWorker));
yield waitUntilSnapshotState(store, [states.SAVING_CENSUS]);
ok(true, "toggling inverted retriggers census");
ok(!getState().inverted, "no long inverted");
dispatch(toggleInverted());
yield waitUntilSnapshotState(store, [states.SAVED_CENSUS]);
ok(getState().inverted, "inverted again");
ok(getState().snapshots[0].inverted,
"census-we-were-in-the-middle-of-recomputing should be inverted again");
heapWorker.destroy();
yield front.detach();
});

View File

@ -0,0 +1,29 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test toggling the top level inversion state of the tree.
let { toggleInverted } = require("devtools/client/memory/actions/inverted");
function run_test() {
run_next_test();
}
add_task(function *() {
let front = new StubbedMemoryFront();
let heapWorker = new HeapAnalysesClient();
yield front.attach();
let store = Store();
const { getState, dispatch } = store;
equal(getState().inverted, false, "not inverted by default");
dispatch(toggleInverted());
equal(getState().inverted, true, "now inverted after toggling");
dispatch(toggleInverted());
equal(getState().inverted, false, "not inverted again after toggling again");
heapWorker.destroy();
yield front.detach();
});

View File

@ -5,6 +5,9 @@ tail =
firefox-appdir = browser
skip-if = toolkit == 'android' || toolkit == 'gonk'
[test_action-toggle-inverted.js]
[test_action-toggle-inverted-and-refresh-01.js]
[test_action-toggle-inverted-and-refresh-02.js]
[test_action-toggle-recording-allocations.js]
[test_action-select-snapshot.js]
[test_action-set-breakdown.js]

View File

@ -2,24 +2,17 @@
* 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/. */
const { assert } = require("devtools/shared/DevToolsUtils");
const { Preferences } = require("resource://gre/modules/Preferences.jsm");
const CUSTOM_BREAKDOWN_PREF = "devtools.memory.custom-breakdowns";
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
const { snapshotState: states, breakdowns } = require("./constants");
const FULL_ERROR_TEXT = "There was an error processing this snapshot.";
const ERROR_SNAPSHOT_TEXT = "⚠ Error!";
const SAVING_SNAPSHOT_TEXT = "Saving snapshot...";
const READING_SNAPSHOT_TEXT = "Reading snapshot...";
const SAVING_CENSUS_TEXT = "Taking heap census...";
// @TODO 1215606
// Use DevToolsUtils.assert when fixed.
exports.assert = function (condition, message) {
if (!condition) {
const err = new Error("Assertion failure: " + message);
DevToolsUtils.reportException("DevToolsUtils.assert", err);
throw err;
}
};
/**
* Returns an array of objects with the unique key `name`
* and `displayName` for each breakdown.
@ -94,15 +87,18 @@ exports.breakdownNameToSpec = function (name) {
/**
* Returns a string representing a readable form of the snapshot's state.
* More brief than `getSnapshotStatusText`.
*
* @param {Snapshot} snapshot
* @return {String}
*/
exports.getSnapshotStatusText = function (snapshot) {
exports.assert((snapshot || {}).state,
assert((snapshot || {}).state,
`Snapshot must have expected state, found ${(snapshot || {}).state}.`);
switch (snapshot.state) {
case states.ERROR:
return ERROR_SNAPSHOT_TEXT;
case states.SAVING:
return SAVING_SNAPSHOT_TEXT;
case states.SAVED:
@ -118,10 +114,27 @@ exports.getSnapshotStatusText = function (snapshot) {
return "";
}
DevToolsUtils.reportException(`Snapshot in unexpected state: ${snapshot.state}`);
assert(false, `Snapshot in unexpected state: ${snapshot.state}`);
return "";
}
/**
* Returns a string representing a readable form of the snapshot's state;
* more verbose than `getSnapshotStatusText`.
*
* @param {Snapshot} snapshot
* @return {String}
*/
exports.getSnapshotStatusTextFull = function (snapshot) {
assert((snapshot || {}).state,
`Snapshot must have expected state, found ${(snapshot || {}).state}.`);
switch (snapshot.state) {
case states.ERROR:
return FULL_ERROR_TEXT;
}
return exports.getSnapshotStatusText(snapshot);
}
/**
* Takes an array of snapshots and a snapshot and returns
* the snapshot instance in `snapshots` that matches

View File

@ -3091,7 +3091,8 @@
if (cm.state.focused && op.updateInput)
cm.display.input.reset(op.typing);
if (op.focus && op.focus == activeElt()) ensureFocus(op.cm);
if (op.focus && op.focus == activeElt() && (!document.hasFocus || document.hasFocus()))
ensureFocus(op.cm);
}
function endOperation_finish(op) {

View File

@ -25,7 +25,8 @@ html, .theme-body, #app, #memory-tool, #memory-tool-container {
.theme-body {
overflow: hidden;
background-color: var(--theme-body-background);
/* Not sure why .theme-body declares it as `background` and overrides */
background-color: var(--theme-toolbar-background) !important;
}
#memory-tool-container {
@ -45,7 +46,7 @@ html, .theme-body, #app, #memory-tool, #memory-tool-container {
* toolbars.inc.css contains definitions for .devtools-button,
* I wager that many of the below styles can be rolled into that
*/
.devtools-button.take-snapshot {
.devtools-toolbar .devtools-button.take-snapshot {
margin: 2px 1px;
padding: 1px;
border-width: 0px;
@ -55,7 +56,7 @@ html, .theme-body, #app, #memory-tool, #memory-tool-container {
display: -moz-box;
}
.devtools-button.take-snapshot::before {
.devtools-toolbar .devtools-button.take-snapshot::before {
background-image: url(images/command-screenshot.png);
-moz-appearance: none;
width: 16px;
@ -65,7 +66,7 @@ html, .theme-body, #app, #memory-tool, #memory-tool-container {
background-repeat: no-repeat;
}
@media (min-resolution: 1.1dppx) {
.devtools-button.take-snapshot::before {
.devtools-toolbar .devtools-button.take-snapshot::before {
background-image: url(images/command-screenshot@2x.png);
}
}
@ -109,9 +110,10 @@ html, .theme-body, #app, #memory-tool, #memory-tool-container {
height: 100%;
overflow-y: scroll;
background-color: var(--theme-toolbar-background);
border-color: var(--theme-splitter-color);
color: var(--theme-body-color-alt);
border-left-width: 1px;
border-color: var(--theme-splitter-color);
border-style: solid;
border-width: 0px 1px 0px 0px;
}
.list > li {
@ -120,6 +122,7 @@ html, .theme-body, #app, #memory-tool, #memory-tool-container {
border-bottom: 1px solid rgba(128,128,128,0.15);
padding: 8px;
cursor: pointer;
color: var(--theme-selection-color);
}
.list > li.selected {
@ -148,16 +151,37 @@ html, .theme-body, #app, #memory-tool, #memory-tool-container {
#heap-view {
flex: 1 1 auto;
border-color: var(--theme-splitter-color);
color: var(--theme-body-color)
color: var(--theme-body-color);
border-left-width: 1px;
}
#heap-view .heap-view-panel {
width: 100%;
height: 100%;
background-color: var(--theme-toolbar-background)
}
#heap-view .heap-view-panel .snapshot-status, #heap-view .take-snapshot {
font-size: 120%;
display: block;
margin: 65px auto;
}
#heap-view .heap-view-panel .snapshot-status {
width: 500px;
text-align: center;
}
#heap-view .take-snapshot {
padding: 5px;
}
#heap-view .heap-view-panel[data-state="snapshot-state-error"] pre {
background-color: var(--theme-body-background);
overflow-y: scroll;
height: 100px;
margin: 20px;
padding: 20px;
}
/**
@ -171,6 +195,7 @@ html, .theme-body, #app, #memory-tool, #memory-tool-container {
.tree {
height: 100%;
overflow-y: scroll;
background-color: var(--theme-body-background);
}
.tree .header {
@ -206,8 +231,9 @@ html, .theme-body, #app, #memory-tool, #memory-tool-container {
background-color: var(--row-hover-background-color);
}
.tree-node:focus {
.tree-node:focus, .heap-tree-item.focused {
background-color: var(--theme-selection-background);
color: var(--theme-selection-color);
}
.header {
@ -244,3 +270,20 @@ html, .theme-body, #app, #memory-tool, #memory-tool-container {
.heap-tree-item-name {
padding-left: 10px;
}
.error::before {
content: "";
background-image: url(chrome://devtools/skin/themes/images/webconsole.svg);
background-repeat: no-repeat;
background-size: 72px 60px;
width: 12px;
height: 12px;
display: inline-block;
background-position: -24px -24px;
margin: 2px 5px 0 0;
max-height: 12px;
}
.theme-light .error::before {
background-image: url(chrome://devtools/skin/themes/images/webconsole.svg#light-icons);
}

View File

@ -79,7 +79,7 @@
outline-offset: -4px;
}
.devtools-toolbarbutton[standalone] {
.devtools-toolbarbutton[standalone], .devtools-toolbarbutton[data-standalone] {
-moz-margin-end: 5px;
border-width: 1px;
border-style: solid;
@ -164,10 +164,12 @@
/* Text-only buttons */
.theme-light .devtools-toolbarbutton[label]:not([text-as-image]):not([type=menu-button]),
.theme-light .devtools-toolbarbutton[data-text-only],
.theme-light #toolbox-buttons .devtools-toolbarbutton[text-as-image] {
background-color: rgba(170, 170, 170, .2); /* Splitter */
}
.theme-dark .devtools-toolbarbutton[label]:not([text-as-image]):not([type=menu-button]),
.theme-dark .devtools-toolbarbutton[data-text-only],
.theme-dark #toolbox-buttons .devtools-toolbarbutton[text-as-image] {
background-color: rgba(0, 0, 0, .2); /* Splitter */
}
@ -270,7 +272,7 @@
position: relative;
}
.devtools-button[standalone] {
.devtools-button[standalone], .devtools-button[data-standalone] {
min-height: 32px;
border-width: 1px;
}

View File

@ -818,3 +818,10 @@ exports.isGenerator = function (fn) {
exports.isPromise = function (p) {
return p && typeof p.then === "function";
};
/**
* Return true if `thing` is a SavedFrame, false otherwise.
*/
exports.isSavedFrame = function (thing) {
return Object.prototype.toString.call(thing) === "[object SavedFrame]";
};

View File

@ -110,7 +110,7 @@ message Node {
bytes typeName = 2;
uint64 typeNameRef = 3;
}
optional uint64 size = 4;
repeated Edge edges = 5;
optional StackFrame allocationStack = 6;

View File

@ -104,6 +104,15 @@ parseMessage(ZeroCopyInputStream& stream, MessageType& message)
// 64MB limit is applied per-message rather than to the whole stream.
CodedInputStream codedStream(&stream);
// The protobuf message nesting that core dumps exhibit is dominated by
// allocation stacks' frames. In the most deeply nested case, each frame has
// two messages: a StackFrame message and a StackFrame::Data message. These
// frames are on top of a small constant of other messages. There are a
// MAX_STACK_DEPTH number of frames, so we multiply this by 3 to make room for
// the two messages per frame plus some head room for the constant number of
// non-dominating messages.
codedStream.SetRecursionLimit(HeapSnapshot::MAX_STACK_DEPTH * 3);
// Because protobuf messages aren't self-delimiting, we serialize each message
// preceeded by its size in bytes. When deserializing, we read this size and
// then limit reading from the stream to the given byte size. If we didn't,
@ -115,7 +124,8 @@ parseMessage(ZeroCopyInputStream& stream, MessageType& message)
auto limit = codedStream.PushLimit(size);
if (NS_WARN_IF(!message.ParseFromCodedStream(&codedStream)) ||
NS_WARN_IF(!codedStream.ConsumedEntireMessage()))
NS_WARN_IF(!codedStream.ConsumedEntireMessage()) ||
NS_WARN_IF(codedStream.BytesUntilLimit() != 0))
{
return false;
}
@ -909,7 +919,8 @@ class MOZ_STACK_CLASS StreamWriter : public CoreDumpWriter
return true;
}
protobuf::StackFrame* getProtobufStackFrame(JS::ubi::StackFrame& frame) {
protobuf::StackFrame* getProtobufStackFrame(JS::ubi::StackFrame& frame,
size_t depth = 1) {
// NB: de-duplicated string properties must be written in the same order
// here as they are read in `HeapSnapshot::saveStackFrame` or else indices
// in references to already serialized strings will be off.
@ -957,8 +968,8 @@ class MOZ_STACK_CLASS StreamWriter : public CoreDumpWriter
}
auto parent = frame.parent();
if (parent) {
auto protobufParent = getProtobufStackFrame(parent);
if (parent && depth < HeapSnapshot::MAX_STACK_DEPTH) {
auto protobufParent = getProtobufStackFrame(parent, depth + 1);
if (!protobufParent)
return nullptr;
data->set_allocated_parent(protobufParent);

View File

@ -71,6 +71,13 @@ class HeapSnapshot final : public nsISupports
bool saveStackFrame(const protobuf::StackFrame& frame,
StackFrameId& outFrameId);
public:
// The maximum number of stack frames that we will serialize into a core
// dump. This helps prevent over-recursion in the protobuf library when
// deserializing stacks.
static const size_t MAX_STACK_DEPTH = 60;
private:
// If present, a timestamp in the same units that `PR_Now` gives.
Maybe<uint64_t> timestamp;

View File

@ -0,0 +1,70 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that we can save a core dump with very deep allocation stacks and read
// it back into a HeapSnapshot.
function stackDepth(stack) {
return stack ? 1 + stackDepth(stack.parent) : 0;
}
function run_test() {
// Create a Debugger observing a debuggee's allocations.
const debuggee = new Cu.Sandbox(null);
const dbg = new Debugger(debuggee);
dbg.memory.trackingAllocationSites = true;
// Allocate some objects in the debuggee that will have their allocation
// stacks recorded by the Debugger.
debuggee.eval("this.objects = []");
debuggee.eval(
(function recursiveAllocate(n) {
if (n <= 0)
return;
// Make sure to recurse before pushing the object so that when TCO is
// implemented sometime in the future, it doesn't invalidate this test.
recursiveAllocate(n - 1);
this.objects.push({});
}).toString()
);
debuggee.eval("recursiveAllocate = recursiveAllocate.bind(this);");
debuggee.eval("recursiveAllocate(200);");
// Now save a snapshot that will include the allocation stacks and read it
// back again.
const filePath = ChromeUtils.saveHeapSnapshot({ runtime: true });
ok(true, "Should be able to save a snapshot.");
const snapshot = ChromeUtils.readHeapSnapshot(filePath);
ok(snapshot, "Should be able to read a heap snapshot");
ok(snapshot instanceof HeapSnapshot, "Should be an instanceof HeapSnapshot");
const report = snapshot.takeCensus({
breakdown: { by: "allocationStack",
then: { by: "count", bytes: true, count: true },
noStack: { by: "count", bytes: true, count: true }
}
});
// Keep this synchronized with `HeapSnapshot::MAX_STACK_DEPTH`!
const MAX_STACK_DEPTH = 60;
let foundStacks = false;
report.forEach((v, k) => {
if (k === "noStack") {
return;
}
foundStacks = true;
const depth = stackDepth(k);
dumpn("Stack depth is " + depth);
ok(depth <= MAX_STACK_DEPTH,
"Every stack should have depth less than or equal to the maximum stack depth");
});
ok(foundStacks);
do_test_finished();
}

View File

@ -34,6 +34,7 @@ support-files =
[test_HeapAnalyses_takeCensus_06.js]
[test_HeapAnalyses_takeCensus_07.js]
[test_HeapSnapshot_creationTime_01.js]
[test_HeapSnapshot_deepStack_01.js]
[test_HeapSnapshot_takeCensus_01.js]
[test_HeapSnapshot_takeCensus_02.js]
[test_HeapSnapshot_takeCensus_03.js]

View File

@ -16,12 +16,14 @@ import android.content.Intent;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
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.restrictions.Restriction;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.setup.SyncAccounts;
import org.mozilla.gecko.util.EventCallback;
@ -32,6 +34,8 @@ import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URISyntaxException;
import java.security.GeneralSecurityException;
import java.util.HashMap;
import java.util.Map;
/**
* Helper class to manage Android Accounts corresponding to Firefox Accounts.
@ -57,7 +61,8 @@ public class AccountsHelper implements NativeEventListener {
"Accounts:Create",
"Accounts:DeleteFirefoxAccount",
"Accounts:Exist",
"Accounts:ProfileUpdated");
"Accounts:ProfileUpdated",
"Accounts:ShowSyncPreferences");
}
public synchronized void uninit() {
@ -72,7 +77,8 @@ public class AccountsHelper implements NativeEventListener {
"Accounts:Create",
"Accounts:DeleteFirefoxAccount",
"Accounts:Exist",
"Accounts:ProfileUpdated");
"Accounts:ProfileUpdated",
"Accounts:ShowSyncPreferences");
}
@Override
@ -113,6 +119,26 @@ public class AccountsHelper implements NativeEventListener {
profileServerEndpoint,
state,
AndroidFxAccount.DEFAULT_AUTHORITIES_TO_SYNC_AUTOMATICALLY_MAP);
final String[] declinedSyncEngines = json.optStringArray("declinedSyncEngines", null);
if (declinedSyncEngines != null) {
Log.i(LOGTAG, "User has selected engines; storing to prefs.");
final Map<String, Boolean> selectedEngines = new HashMap<String, Boolean>();
for (String enabledSyncEngine : SyncConfiguration.validEngineNames()) {
selectedEngines.put(enabledSyncEngine, true);
}
for (String declinedSyncEngine : declinedSyncEngines) {
selectedEngines.put(declinedSyncEngine, false);
}
// The "forms" engine has the same state as the "history" engine.
selectedEngines.put("forms", selectedEngines.get("history"));
FxAccountUtils.pii(LOGTAG, "User selected engines: " + selectedEngines.toString());
try {
SyncConfiguration.storeSelectedEnginesToPrefs(fxAccount.getSyncPrefs(), selectedEngines);
} catch (UnsupportedEncodingException | GeneralSecurityException e) {
Log.e(LOGTAG, "Got exception storing selected engines; ignoring.", e);
}
}
} catch (URISyntaxException | GeneralSecurityException | UnsupportedEncodingException e) {
Log.w(LOGTAG, "Got exception creating Firefox Account from JSON; ignoring.", e);
if (callback != null) {
@ -270,6 +296,17 @@ public class AccountsHelper implements NativeEventListener {
}
final AndroidFxAccount androidFxAccount = new AndroidFxAccount(mContext, account);
androidFxAccount.fetchProfileJSON();
} else if ("Accounts:ShowSyncPreferences".equals(event)) {
final Account account = FirefoxAccounts.getFirefoxAccount(mContext);
if (account == null) {
Log.w(LOGTAG, "Can't change show Sync preferences of non-existent Firefox Account!; ignored");
return;
}
// We don't necessarily have an Activity context here, so we always start in a new task.
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_STATUS);
intent.setFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
mContext.startActivity(intent);
}
}
}

View File

@ -116,6 +116,20 @@ public class AppConstants {
false;
//#endif
public static final boolean MOZ_ANDROID_GCM =
//#ifdef MOZ_ANDROID_GCM
true;
//#else
false;
//#endif
public static final String MOZ_ANDROID_GCM_SENDERID =
//#ifdef MOZ_ANDROID_GCM_SENDERID
"@MOZ_ANDROID_GCM_SENDERID@";
//#else
null;
//#endif
public static final String MOZ_CHILD_PROCESS_NAME = "@MOZ_CHILD_PROCESS_NAME@";
public static final String MOZ_UPDATE_CHANNEL = "@MOZ_UPDATE_CHANNEL@";
public static final String OMNIJAR_NAME = "@OMNIJAR_NAME@";

View File

@ -18,12 +18,16 @@ import org.mozilla.gecko.animation.ViewHelper;
import org.mozilla.gecko.db.BrowserContract.Combined;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.SuggestedSites;
import org.mozilla.gecko.db.TabsAccessor;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.favicons.Favicons;
import org.mozilla.gecko.favicons.LoadFaviconTask;
import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
import org.mozilla.gecko.favicons.decoders.IconDirectoryEntry;
import org.mozilla.gecko.firstrun.FirstrunPane;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.activities.FxAccountWebFlowActivity;
import org.mozilla.gecko.gfx.DynamicToolbarAnimator;
import org.mozilla.gecko.gfx.ImmutableViewportMetrics;
import org.mozilla.gecko.gfx.LayerView;
@ -82,6 +86,7 @@ import org.mozilla.gecko.widget.ButtonToast;
import org.mozilla.gecko.widget.ButtonToast.ToastListener;
import org.mozilla.gecko.widget.GeckoActionProvider;
import android.accounts.Account;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.ContentResolver;
@ -104,6 +109,8 @@ import android.nfc.NfcEvent;
import android.os.Build;
import android.os.Bundle;
import android.os.StrictMode;
import android.support.annotation.NonNull;
import android.support.annotation.WorkerThread;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.view.MenuItemCompat;
@ -3229,16 +3236,13 @@ public class BrowserApp extends GeckoApp
if (itemId == R.id.send_to_device) {
tab = Tabs.getInstance().getSelectedTab();
if (tab != null) {
String url = tab.getURL();
if (url != null) {
if (AboutPages.isAboutReader(url)) {
url = ReaderModeUtils.getUrlFromAboutReader(url);
final Tab selectedTab = tab;
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
handleSendToDevice(selectedTab);
}
Intent sendToDeviceIntent = GeckoAppShell.getShareIntent(getContext(), url,
"text/plain", tab.getDisplayTitle());
sendToDeviceIntent.setClass(getContext(), ShareDialog.class);
startActivity(sendToDeviceIntent);
}
});
}
return true;
}
@ -3368,6 +3372,46 @@ public class BrowserApp extends GeckoApp
return super.onOptionsItemSelected(item);
}
/**
* Handles a press to the send to device button in the browser menu. The
* expected states when the user presses the button are:
* * Not signed in: open to FxA sign-up
* * Signed in but no other devices: display toast with a message
* explaining they should connect another device to use this feature
* * Signed in but >= 1 other device: display device list
*/
@WorkerThread
private void handleSendToDevice(@NonNull final Tab selectedTab) {
final Account account = FirefoxAccounts.getFirefoxAccount(this);
if (account == null) {
// TODO (bug 1217164): Go back to previous tab on back press
final Intent intent = new Intent(FxAccountConstants.ACTION_FXA_GET_STARTED);
intent.putExtra(FxAccountWebFlowActivity.EXTRA_ENDPOINT, FxAccountConstants.ENDPOINT_PREFERENCES);
startActivity(intent);
return;
}
final BrowserDB browserDB = GeckoProfile.get(this).getDB();
final TabsAccessor tabsAccessor = browserDB.getTabsAccessor();
final int remoteClientCount = tabsAccessor.getRemoteClientCount(this);
if (remoteClientCount == 0) {
final Toast toast = Toast.makeText(this, R.string.menu_no_synced_devices, Toast.LENGTH_LONG);
toast.show();
} else {
String url = selectedTab.getURL();
if (url != null) {
if (AboutPages.isAboutReader(url)) {
url = ReaderModeUtils.getUrlFromAboutReader(url);
}
final Intent sendToDeviceIntent = GeckoAppShell.getShareIntent(getContext(), url,
"text/plain", selectedTab.getDisplayTitle());
sendToDeviceIntent.setClass(getContext(), ShareDialog.class);
startActivity(sendToDeviceIntent);
}
}
}
@Override
public boolean onMenuItemLongClick(MenuItem item) {
if (item.getItemId() == R.id.reload) {

View File

@ -74,6 +74,14 @@ ifdef MOZ_NATIVE_DEVICES
$(NULL)
endif
ifdef MOZ_ANDROID_GCM
JAVA_CLASSPATH += \
$(ANDROID_PLAY_SERVICES_BASE_AAR_LIB) \
$(ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB) \
$(ANDROID_PLAY_SERVICES_GCM_AAR_LIB) \
$(NULL)
endif
JAVA_CLASSPATH := $(subst $(NULL) ,:,$(strip $(JAVA_CLASSPATH)))
# Library jars that we're bundling: these are subject to Proguard before inclusion
@ -96,6 +104,18 @@ ifdef MOZ_NATIVE_DEVICES
$(NULL)
endif
ifdef MOZ_ANDROID_GCM
java_bundled_libs += \
$(ANDROID_PLAY_SERVICES_BASE_AAR_LIB) \
$(ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB) \
$(ANDROID_PLAY_SERVICES_GCM_AAR_LIB) \
$(NULL)
endif
# uniq purloined from http://stackoverflow.com/a/16151140.
uniq = $(if $1,$(firstword $1) $(call uniq,$(filter-out $(firstword $1),$1)))
java_bundled_libs := $(call uniq,$(java_bundled_libs))
java_bundled_libs := $(subst $(NULL) ,:,$(strip $(java_bundled_libs)))
# All the jars we're compiling from source. (not to be confused with
@ -371,6 +391,7 @@ generated/android/support/v7/recyclerview/R.java: .aapt.deps ;
generated/com/google/android/gms/R.java: .aapt.deps ;
generated/com/google/android/gms/base/R.java: .aapt.deps ;
generated/com/google/android/gms/cast/R.java: .aapt.deps ;
generated/com/google/android/gms/gcm/R.java: .aapt.deps ;
gecko.ap_: .aapt.deps ;
R.txt: .aapt.deps ;

View File

@ -300,16 +300,13 @@ public class BrowserContract {
public static final String POSITION = "position";
}
public static final class Clients {
public static final class Clients implements CommonColumns {
private Clients() {}
public static final Uri CONTENT_RECENCY_URI = Uri.withAppendedPath(TABS_AUTHORITY_URI, "clients_recency");
public static final Uri CONTENT_URI = Uri.withAppendedPath(TABS_AUTHORITY_URI, "clients");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/client";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/client";
// Implicit rowid in SQL table.
public static final String ROWID = "rowid";
// Client-provided name string. Could conceivably be null.
public static final String NAME = "name";

View File

@ -39,7 +39,7 @@ import android.util.Log;
final class BrowserDatabaseHelper extends SQLiteOpenHelper {
private static final String LOGTAG = "GeckoBrowserDBHelper";
public static final int DATABASE_VERSION = 24;
public static final int DATABASE_VERSION = 25;
public static final String DATABASE_NAME = "browser.db";
final protected Context mContext;
@ -197,12 +197,13 @@ final class BrowserDatabaseHelper extends SQLiteOpenHelper {
" ON " + TABLE_CLIENTS + "(" + BrowserContract.Clients.GUID + ")");
}
private void createTabsTable(SQLiteDatabase db) {
private boolean didCreateTabsTable = false;
private void createTabsTable(SQLiteDatabase db, final String tableName) {
debug("Creating tabs.db: " + db.getPath());
debug("Creating " + TABLE_TABS + " table");
debug("Creating " + tableName + " table");
// Table for each tab on any client.
db.execSQL("CREATE TABLE " + TABLE_TABS + "(" +
db.execSQL("CREATE TABLE " + tableName + "(" +
BrowserContract.Tabs._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
BrowserContract.Tabs.CLIENT_GUID + " TEXT," +
BrowserContract.Tabs.TITLE + " TEXT," +
@ -210,14 +211,18 @@ final class BrowserDatabaseHelper extends SQLiteOpenHelper {
BrowserContract.Tabs.HISTORY + " TEXT," +
BrowserContract.Tabs.FAVICON + " TEXT," +
BrowserContract.Tabs.LAST_USED + " INTEGER," +
BrowserContract.Tabs.POSITION + " INTEGER" +
BrowserContract.Tabs.POSITION + " INTEGER, " +
"FOREIGN KEY (" + BrowserContract.Tabs.CLIENT_GUID + ") REFERENCES " +
TABLE_CLIENTS + "(" + BrowserContract.Clients.GUID + ") ON DELETE CASCADE" +
");");
}
private void createTabsTableIndices(SQLiteDatabase db, final String tableName) {
// Indices on CLIENT_GUID and POSITION.
db.execSQL("CREATE INDEX " + TabsProvider.INDEX_TABS_GUID +
" ON " + TABLE_TABS + "(" + BrowserContract.Tabs.CLIENT_GUID + ")");
" ON " + tableName + "(" + BrowserContract.Tabs.CLIENT_GUID + ")");
db.execSQL("CREATE INDEX " + TabsProvider.INDEX_TABS_POSITION +
" ON " + TABLE_TABS + "(" + BrowserContract.Tabs.POSITION + ")");
" ON " + tableName + "(" + BrowserContract.Tabs.POSITION + ")");
}
// Insert a client row for our local Fennec client.
@ -341,9 +346,12 @@ final class BrowserDatabaseHelper extends SQLiteOpenHelper {
createHistoryTable(db);
createFaviconsTable(db);
createThumbnailsTable(db);
createTabsTable(db);
createClientsTable(db);
createLocalClient(db);
createTabsTable(db, TABLE_TABS);
didCreateTabsTable = true;
createTabsTableIndices(db, TABLE_TABS);
createBookmarksWithFaviconsView(db);
createHistoryWithFaviconsView(db);
@ -366,8 +374,10 @@ final class BrowserDatabaseHelper extends SQLiteOpenHelper {
* @param destinationDB The destination database.
*/
public void copyTabsDB(File tabsDBFile, SQLiteDatabase destinationDB) {
createTabsTable(destinationDB);
createClientsTable(destinationDB);
createTabsTable(destinationDB, TABLE_TABS);
didCreateTabsTable = true;
createTabsTableIndices(destinationDB, TABLE_TABS);
SQLiteDatabase oldTabsDB = null;
try {
@ -987,6 +997,41 @@ final class BrowserDatabaseHelper extends SQLiteOpenHelper {
}
}
private void upgradeDatabaseFrom24to25(SQLiteDatabase db) {
if (didCreateTabsTable) {
debug("No need to rev tabs schema; foreign key constraint exists.");
return;
}
debug("Rewriting tabs table.");
createTabsTable(db, "tmp_tabs");
// Remove indexes. We don't need them now, and we'll be throwing away the table.
db.execSQL("DROP INDEX IF EXISTS " + TabsProvider.INDEX_TABS_GUID);
db.execSQL("DROP INDEX IF EXISTS " + TabsProvider.INDEX_TABS_POSITION);
db.execSQL("INSERT INTO tmp_tabs (" +
// Here are the columns we can preserve.
BrowserContract.Tabs._ID + ", " +
BrowserContract.Tabs.CLIENT_GUID + ", " +
BrowserContract.Tabs.TITLE + ", " +
BrowserContract.Tabs.URL + ", " +
BrowserContract.Tabs.HISTORY + ", " +
BrowserContract.Tabs.FAVICON + ", " +
BrowserContract.Tabs.LAST_USED + ", " +
BrowserContract.Tabs.POSITION +
") " +
"SELECT " +
"_id, client_guid, title, url, history, favicon, last_used, position" +
" FROM " + TABLE_TABS);
// Now switch these tables over and recreate the indices.
db.execSQL("DROP TABLE " + TABLE_TABS);
db.execSQL("ALTER TABLE tmp_tabs RENAME TO " + TABLE_TABS);
createTabsTableIndices(db, TABLE_TABS);
didCreateTabsTable =true;
}
private void createV19CombinedView(SQLiteDatabase db) {
db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED);
db.execSQL("DROP VIEW IF EXISTS " + VIEW_COMBINED_WITH_FAVICONS);
@ -1062,6 +1107,10 @@ final class BrowserDatabaseHelper extends SQLiteOpenHelper {
case 24:
upgradeDatabaseFrom23to24(db);
break;
case 25:
upgradeDatabaseFrom24to25(db);
break;
}
}

View File

@ -72,6 +72,20 @@ public class LocalTabsAccessor implements TabsAccessor {
clientsRecencyUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Clients.CONTENT_RECENCY_URI);
}
@Override
public int getRemoteClientCount(final Context context) {
final Cursor remoteClientsCursor = getRemoteClientsByRecencyCursor(context);
if (remoteClientsCursor == null) {
return 0;
}
try {
return remoteClientsCursor.getCount();
} finally {
remoteClientsCursor.close();
}
}
/**
* Extracts a List of just RemoteClients from a cursor.
* The supplied cursor should be grouped by guid and sorted by most recently used.

View File

@ -117,6 +117,11 @@ class StubTabsAccessor implements TabsAccessor {
public StubTabsAccessor() {
}
@Override
public int getRemoteClientCount(Context context) {
return 0;
}
@Override
public List<RemoteClient> getClientsWithoutTabsByRecencyFromCursor(Cursor cursor) {
return new ArrayList<>();

View File

@ -17,6 +17,7 @@ public interface TabsAccessor {
public void onQueryTabsComplete(List<RemoteClient> clients);
}
public int getRemoteClientCount(Context context);
public Cursor getRemoteClientsByRecencyCursor(Context context);
public Cursor getRemoteTabsCursor(Context context);
public Cursor getRemoteTabsCursor(Context context, int limit);

View File

@ -162,14 +162,13 @@ public class TabsProvider extends SharedBrowserDatabaseProvider {
switch (match) {
case CLIENTS_ID:
trace("Delete on CLIENTS_ID: " + uri);
selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients.ROWID));
selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients._ID));
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { Long.toString(ContentUris.parseId(uri)) });
// fall through
case CLIENTS:
trace("Delete on CLIENTS: " + uri);
// Delete from both TABLE_TABS and TABLE_CLIENTS.
deleteValues(uri, selection, selectionArgs, TABLE_TABS);
deleted = deleteValues(uri, selection, selectionArgs, TABLE_CLIENTS);
break;
@ -236,7 +235,7 @@ public class TabsProvider extends SharedBrowserDatabaseProvider {
switch (match) {
case CLIENTS_ID:
trace("Update on CLIENTS_ID: " + uri);
selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients.ROWID));
selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients._ID));
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { Long.toString(ContentUris.parseId(uri)) });
// fall through
@ -297,7 +296,7 @@ public class TabsProvider extends SharedBrowserDatabaseProvider {
case CLIENTS_ID:
trace("Query is on CLIENTS_ID: " + uri);
selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients.ROWID));
selection = DBUtils.concatenateWhere(selection, selectColumn(TABLE_CLIENTS, Clients._ID));
selectionArgs = DBUtils.appendSelectionArgs(selectionArgs,
new String[] { Long.toString(ContentUris.parseId(uri)) });
// fall through

View File

@ -145,6 +145,12 @@
are found. -->
<!ENTITY overlay_no_synced_devices "No Firefox Account connected devices found">
<!-- Localization note (menu_no_synced_devices): Used in a toast when the user
clicks on the button to send a tab to another device and there are no
other devices present. This label should briefly inform the user that they
need another connected device in order to use the feature. -->
<!ENTITY menu_no_synced_devices "Send this tab to another connected device">
<!ENTITY pref_category_search3 "Search">
<!ENTITY pref_category_search_summary "Customize your search providers">
<!ENTITY pref_category_display "Display">

View File

@ -690,6 +690,28 @@ if CONFIG['MOZ_NATIVE_DEVICES']:
ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_CAST_AAR_RES']]
resjar.generated_sources += ['com/google/android/gms/cast/R.java']
if CONFIG['MOZ_ANDROID_GCM']:
gbjar.extra_jars += [
CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR_LIB'],
CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR_LIB'],
CONFIG['ANDROID_PLAY_SERVICES_GCM_AAR_LIB'],
]
if CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR']:
ANDROID_EXTRA_PACKAGES += ['com.google.android.gms']
ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_BASE_AAR_RES']]
resjar.generated_sources += ['com/google/android/gms/R.java']
if CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR']:
ANDROID_EXTRA_PACKAGES += ['com.google.android.gms']
ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_BASEMENT_AAR_RES']]
resjar.generated_sources += ['com/google/android/gms/R.java']
if CONFIG['ANDROID_PLAY_SERVICES_GCM_AAR']:
ANDROID_EXTRA_PACKAGES += ['com.google.android.gms.gcm']
ANDROID_EXTRA_RES_DIRS += ['%' + CONFIG['ANDROID_PLAY_SERVICES_GCM_AAR_RES']]
resjar.generated_sources += ['com/google/android/gms/gcm/R.java']
gbjar.extra_jars += [CONFIG['ANDROID_APPCOMPAT_V7_AAR_LIB']]
gbjar.extra_jars += [CONFIG['ANDROID_DESIGN_AAR_LIB']]
gbjar.extra_jars += [CONFIG['ANDROID_RECYCLERVIEW_V7_AAR_LIB']]
@ -835,11 +857,12 @@ ANDROID_ASSETS_DIRS += [
for var in ('MOZ_ANDROID_ANR_REPORTER', 'MOZ_LINKER_EXTRACT', 'MOZ_DEBUG',
'MOZ_ANDROID_SEARCH_ACTIVITY', 'MOZ_NATIVE_DEVICES', 'MOZ_ANDROID_MLS_STUMBLER',
'MOZ_ANDROID_SHARE_OVERLAY', 'MOZ_ANDROID_DOWNLOADS_INTEGRATION', 'MOZ_INSTALL_TRACKING',
'MOZ_ANDROID_GCM',
'MOZ_ANDROID_TAB_QUEUE', 'MOZ_ANDROID_FIREFOX_ACCOUNT_PROFILES'):
if CONFIG[var]:
DEFINES[var] = 1
for var in ('MOZ_UPDATER', 'MOZ_PKG_SPECIAL'):
for var in ('MOZ_UPDATER', 'MOZ_PKG_SPECIAL', 'MOZ_ANDROID_GCM_SENDERID'):
if CONFIG[var]:
DEFINES[var] = CONFIG[var]

View File

@ -136,6 +136,7 @@
<string name="overlay_share_no_url">&overlay_share_no_url;</string>
<string name="overlay_share_select_device">&overlay_share_select_device;</string>
<string name="overlay_no_synced_devices">&overlay_no_synced_devices;</string>
<string name="menu_no_synced_devices">&menu_no_synced_devices;</string>
<string name="settings">&settings;</string>
<string name="settings_title">&settings_title;</string>

View File

@ -355,32 +355,28 @@ public class FennecTabsRepository extends Repository {
}
/**
* Deletes all non-local clients and remote tabs.
*
* This function doesn't delete non-local clients due to bug in TabsProvider. Refer Bug 1025128.
*
* Upon remote tabs deletion, the clients without tabs are not shown in UI.
* Deletes all non-local clients and their associated remote tabs.
*/
public static void deleteNonLocalClientsAndTabs(Context context) {
final String nonLocalTabsSelection = BrowserContract.Tabs.CLIENT_GUID + " IS NOT NULL";
final String nonLocalClientSelection = BrowserContract.Clients.GUID + " IS NOT NULL";
ContentProviderClient tabsProvider = context.getContentResolver()
.acquireContentProviderClient(BrowserContractHelpers.TABS_CONTENT_URI);
if (tabsProvider == null) {
Logger.warn(LOG_TAG, "Unable to create tabsProvider!");
ContentProviderClient clientsProvider = context.getContentResolver()
.acquireContentProviderClient(BrowserContractHelpers.CLIENTS_CONTENT_URI);
if (clientsProvider == null) {
Logger.warn(LOG_TAG, "Unable to create clientsProvider!");
return;
}
try {
Logger.info(LOG_TAG, "Clearing all non-local tabs for default profile.");
tabsProvider.delete(BrowserContractHelpers.TABS_CONTENT_URI, nonLocalTabsSelection, null);
Logger.info(LOG_TAG, "Clearing all non-local clients and their associated remote tabs for default profile.");
clientsProvider.delete(BrowserContractHelpers.CLIENTS_CONTENT_URI, nonLocalClientSelection, null);
} catch (RemoteException e) {
Logger.warn(LOG_TAG, "Error while deleting", e);
} finally {
try {
tabsProvider.release();
clientsProvider.release();
} catch (Exception e) {
Logger.warn(LOG_TAG, "Got exception releasing tabsProvider!", e);
Logger.warn(LOG_TAG, "Got exception releasing clientsProvider!", e);
}
}
}

View File

@ -102,6 +102,11 @@ MOZ_ANDROID_TAB_QUEUE=1
# Use the low-memory GC tuning.
export JS_GC_SMALL_CHUNK_SIZE=1
# Enable GCM registration on Nightly builds only.
if test "$NIGHTLY_BUILD"; then
MOZ_ANDROID_GCM=1
fi
# Enable Firefox Account avatars.
MOZ_ANDROID_FIREFOX_ACCOUNT_PROFILES=1

View File

@ -128,6 +128,9 @@ spoon {
if (gradle.startParameter.taskNames.contains('runBrowserTests')) {
spoonPackageName = 'org.mozilla.tests.browser.junit3'
}
if (gradle.startParameter.taskNames.contains('runBackgroundTests')) {
spoonPackageName = 'org.mozilla.gecko.background'
}
if (project.hasProperty('spoonPackageName')) {
// Command line overrides everything.
spoonPackageName = project.spoonPackageName
@ -147,4 +150,7 @@ afterEvaluate {
task runBrowserTests {
dependsOn tasks["spoonDebugAndroidTest"]
}
task runBackgroundTests {
dependsOn tasks["spoonDebugAndroidTest"]
}
}

View File

@ -105,6 +105,12 @@ dependencies {
compile 'com.google.android.gms:play-services-cast:8.1.0'
}
if (mozconfig.substs.MOZ_ANDROID_GCM) {
compile 'com.google.android.gms:play-services-basement:8.1.0'
compile 'com.google.android.gms:play-services-base:8.1.0'
compile 'com.google.android.gms:play-services-gcm:8.1.0'
}
compile project(':thirdparty')
testCompile 'junit:junit:4.12'

View File

@ -117,7 +117,7 @@ class MachCommands(MachCommandBase):
# Test code.
srcdir('app/src/robocop_harness/org/mozilla/gecko', 'build/mobile/robocop')
srcdir('app/src/robocop/org/mozilla/gecko/tests', 'mobile/android/tests/browser/robocop')
srcdir('app/src/background/org/mozilla/gecko/background', 'mobile/android/tests/background/junit3/src')
srcdir('app/src/background', 'mobile/android/tests/background/junit3/src')
srcdir('app/src/browser', 'mobile/android/tests/browser/junit3/src')
srcdir('app/src/javaaddons', 'mobile/android/tests/javaaddons/src')
# Test libraries.

View File

@ -158,5 +158,17 @@ var Accounts = Object.freeze({
return Messaging.sendRequestForResult({
type: "Accounts:DeleteFirefoxAccount",
});
},
showSyncPreferences: function () {
// Only show Sync preferences of an existing Android Account.
return Accounts.getFirefoxAccount().then(account => {
if (!account) {
throw new Error("Can't show Sync preferences of non-existent Firefox Account!");
}
return Messaging.sendRequestForResult({
type: "Accounts:ShowSyncPreferences"
});
});
}
});

View File

@ -30,6 +30,7 @@ const COMMAND_LOGIN = "fxaccounts:login";
const COMMAND_CHANGE_PASSWORD = "fxaccounts:change_password";
const COMMAND_DELETE_ACCOUNT = "fxaccounts:delete_account";
const COMMAND_PROFILE_CHANGE = "profile:change";
const COMMAND_SYNC_PREFERENCES = "fxaccounts:sync_preferences";
const PREF_LAST_FXA_USER = "identity.fxaccounts.lastSignedInUserHash";
@ -350,6 +351,13 @@ this.FxAccountsWebChannel.prototype = {
});
break;
case COMMAND_SYNC_PREFERENCES:
Accounts.showSyncPreferences()
.catch(e => {
log.e(e.toString());
});
break;
default:
log.w("Ignoring unrecognized FxAccountsWebChannel command: " + JSON.stringify(command));
break;

View File

@ -5,107 +5,107 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
background_junit3_sources = [
'src/common/TestAndroidLogWriters.java',
'src/common/TestBrowserContractHelpers.java',
'src/common/TestDateUtils.java',
'src/common/TestUtils.java',
'src/common/TestWaitHelper.java',
'src/db/AndroidBrowserRepositoryTestCase.java',
'src/db/TestAndroidBrowserBookmarksRepository.java',
'src/db/TestAndroidBrowserHistoryDataExtender.java',
'src/db/TestAndroidBrowserHistoryRepository.java',
'src/db/TestBookmarks.java',
'src/db/TestCachedSQLiteOpenHelper.java',
'src/db/TestClientsDatabase.java',
'src/db/TestClientsDatabaseAccessor.java',
'src/db/TestFennecTabsRepositorySession.java',
'src/db/TestFennecTabsStorage.java',
'src/db/TestFormHistoryRepositorySession.java',
'src/db/TestPasswordsRepository.java',
'src/fxa/authenticator/TestAccountPickler.java',
'src/fxa/TestAccountLoader.java',
'src/fxa/TestBrowserIDKeyPairGeneration.java',
'src/fxa/TestFirefoxAccounts.java',
'src/healthreport/MockDatabaseEnvironment.java',
'src/healthreport/MockHealthReportDatabaseStorage.java',
'src/healthreport/MockHealthReportSQLiteOpenHelper.java',
'src/healthreport/MockProfileInformationCache.java',
'src/healthreport/prune/TestHealthReportPruneService.java',
'src/healthreport/prune/TestPrunePolicyDatabaseStorage.java',
'src/healthreport/TestEnvironmentBuilder.java',
'src/healthreport/TestEnvironmentV1HashAppender.java',
'src/healthreport/TestHealthReportBroadcastService.java',
'src/healthreport/TestHealthReportDatabaseStorage.java',
'src/healthreport/TestHealthReportGenerator.java',
'src/healthreport/TestHealthReportProvider.java',
'src/healthreport/TestHealthReportSQLiteOpenHelper.java',
'src/healthreport/TestProfileInformationCache.java',
'src/healthreport/upload/TestAndroidSubmissionClient.java',
'src/healthreport/upload/TestHealthReportUploadService.java',
'src/helpers/AndroidSyncTestCase.java',
'src/helpers/BackgroundServiceTestCase.java',
'src/helpers/DBHelpers.java',
'src/helpers/DBProviderTestCase.java',
'src/helpers/FakeProfileTestCase.java',
'src/nativecode/test/TestNativeCrypto.java',
'src/sync/AndroidSyncTestCaseWithAccounts.java',
'src/sync/helpers/BookmarkHelpers.java',
'src/sync/helpers/DefaultBeginDelegate.java',
'src/sync/helpers/DefaultCleanDelegate.java',
'src/sync/helpers/DefaultDelegate.java',
'src/sync/helpers/DefaultFetchDelegate.java',
'src/sync/helpers/DefaultFinishDelegate.java',
'src/sync/helpers/DefaultGuidsSinceDelegate.java',
'src/sync/helpers/DefaultSessionCreationDelegate.java',
'src/sync/helpers/DefaultStoreDelegate.java',
'src/sync/helpers/ExpectBeginDelegate.java',
'src/sync/helpers/ExpectBeginFailDelegate.java',
'src/sync/helpers/ExpectFetchDelegate.java',
'src/sync/helpers/ExpectFetchSinceDelegate.java',
'src/sync/helpers/ExpectFinishDelegate.java',
'src/sync/helpers/ExpectFinishFailDelegate.java',
'src/sync/helpers/ExpectGuidsSinceDelegate.java',
'src/sync/helpers/ExpectInvalidRequestFetchDelegate.java',
'src/sync/helpers/ExpectInvalidTypeStoreDelegate.java',
'src/sync/helpers/ExpectManyStoredDelegate.java',
'src/sync/helpers/ExpectNoGUIDsSinceDelegate.java',
'src/sync/helpers/ExpectNoStoreDelegate.java',
'src/sync/helpers/ExpectStoreCompletedDelegate.java',
'src/sync/helpers/ExpectStoredDelegate.java',
'src/sync/helpers/HistoryHelpers.java',
'src/sync/helpers/PasswordHelpers.java',
'src/sync/helpers/SessionTestHelper.java',
'src/sync/helpers/SimpleSuccessBeginDelegate.java',
'src/sync/helpers/SimpleSuccessCreationDelegate.java',
'src/sync/helpers/SimpleSuccessFetchDelegate.java',
'src/sync/helpers/SimpleSuccessFinishDelegate.java',
'src/sync/helpers/SimpleSuccessStoreDelegate.java',
'src/sync/TestAccountPickler.java',
'src/sync/TestClientsStage.java',
'src/sync/TestConfigurationMigrator.java',
'src/sync/TestResetting.java',
'src/sync/TestSendTabData.java',
'src/sync/TestStoreTracking.java',
'src/sync/TestSyncAccounts.java',
'src/sync/TestSyncAuthenticatorService.java',
'src/sync/TestSyncConfiguration.java',
'src/sync/TestTabsRecord.java',
'src/sync/TestUpgradeRequired.java',
'src/sync/TestWebURLFinder.java',
'src/telemetry/TestTelemetryRecorder.java',
'src/testhelpers/BaseMockServerSyncStage.java',
'src/testhelpers/CommandHelpers.java',
'src/testhelpers/DefaultGlobalSessionCallback.java',
'src/testhelpers/JPakeNumGeneratorFixed.java',
'src/testhelpers/MockAbstractNonRepositorySyncStage.java',
'src/testhelpers/MockClientsDatabaseAccessor.java',
'src/testhelpers/MockClientsDataDelegate.java',
'src/testhelpers/MockGlobalSession.java',
'src/testhelpers/MockPrefsGlobalSession.java',
'src/testhelpers/MockRecord.java',
'src/testhelpers/MockServerSyncStage.java',
'src/testhelpers/MockSharedPreferences.java',
'src/testhelpers/StubDelegate.java',
'src/testhelpers/WaitHelper.java',
'src/testhelpers/WBORepository.java',
'src/org/mozilla/gecko/background/common/TestAndroidLogWriters.java',
'src/org/mozilla/gecko/background/common/TestBrowserContractHelpers.java',
'src/org/mozilla/gecko/background/common/TestDateUtils.java',
'src/org/mozilla/gecko/background/common/TestUtils.java',
'src/org/mozilla/gecko/background/common/TestWaitHelper.java',
'src/org/mozilla/gecko/background/db/AndroidBrowserRepositoryTestCase.java',
'src/org/mozilla/gecko/background/db/TestAndroidBrowserBookmarksRepository.java',
'src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryDataExtender.java',
'src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryRepository.java',
'src/org/mozilla/gecko/background/db/TestBookmarks.java',
'src/org/mozilla/gecko/background/db/TestCachedSQLiteOpenHelper.java',
'src/org/mozilla/gecko/background/db/TestClientsDatabase.java',
'src/org/mozilla/gecko/background/db/TestClientsDatabaseAccessor.java',
'src/org/mozilla/gecko/background/db/TestFennecTabsRepositorySession.java',
'src/org/mozilla/gecko/background/db/TestFennecTabsStorage.java',
'src/org/mozilla/gecko/background/db/TestFormHistoryRepositorySession.java',
'src/org/mozilla/gecko/background/db/TestPasswordsRepository.java',
'src/org/mozilla/gecko/background/fxa/authenticator/TestAccountPickler.java',
'src/org/mozilla/gecko/background/fxa/TestAccountLoader.java',
'src/org/mozilla/gecko/background/fxa/TestBrowserIDKeyPairGeneration.java',
'src/org/mozilla/gecko/background/fxa/TestFirefoxAccounts.java',
'src/org/mozilla/gecko/background/healthreport/MockDatabaseEnvironment.java',
'src/org/mozilla/gecko/background/healthreport/MockHealthReportDatabaseStorage.java',
'src/org/mozilla/gecko/background/healthreport/MockHealthReportSQLiteOpenHelper.java',
'src/org/mozilla/gecko/background/healthreport/MockProfileInformationCache.java',
'src/org/mozilla/gecko/background/healthreport/prune/TestHealthReportPruneService.java',
'src/org/mozilla/gecko/background/healthreport/prune/TestPrunePolicyDatabaseStorage.java',
'src/org/mozilla/gecko/background/healthreport/TestEnvironmentBuilder.java',
'src/org/mozilla/gecko/background/healthreport/TestEnvironmentV1HashAppender.java',
'src/org/mozilla/gecko/background/healthreport/TestHealthReportBroadcastService.java',
'src/org/mozilla/gecko/background/healthreport/TestHealthReportDatabaseStorage.java',
'src/org/mozilla/gecko/background/healthreport/TestHealthReportGenerator.java',
'src/org/mozilla/gecko/background/healthreport/TestHealthReportProvider.java',
'src/org/mozilla/gecko/background/healthreport/TestHealthReportSQLiteOpenHelper.java',
'src/org/mozilla/gecko/background/healthreport/TestProfileInformationCache.java',
'src/org/mozilla/gecko/background/healthreport/upload/TestAndroidSubmissionClient.java',
'src/org/mozilla/gecko/background/healthreport/upload/TestHealthReportUploadService.java',
'src/org/mozilla/gecko/background/helpers/AndroidSyncTestCase.java',
'src/org/mozilla/gecko/background/helpers/BackgroundServiceTestCase.java',
'src/org/mozilla/gecko/background/helpers/DBHelpers.java',
'src/org/mozilla/gecko/background/helpers/DBProviderTestCase.java',
'src/org/mozilla/gecko/background/helpers/FakeProfileTestCase.java',
'src/org/mozilla/gecko/background/nativecode/test/TestNativeCrypto.java',
'src/org/mozilla/gecko/background/sync/AndroidSyncTestCaseWithAccounts.java',
'src/org/mozilla/gecko/background/sync/helpers/BookmarkHelpers.java',
'src/org/mozilla/gecko/background/sync/helpers/DefaultBeginDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/DefaultCleanDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/DefaultDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/DefaultFetchDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/DefaultFinishDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/DefaultGuidsSinceDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/DefaultSessionCreationDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/DefaultStoreDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/ExpectBeginDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/ExpectBeginFailDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/ExpectFetchDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/ExpectFetchSinceDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/ExpectFinishDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/ExpectFinishFailDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/ExpectGuidsSinceDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidRequestFetchDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/ExpectInvalidTypeStoreDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/ExpectManyStoredDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/ExpectNoGUIDsSinceDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/ExpectNoStoreDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/ExpectStoreCompletedDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/ExpectStoredDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/HistoryHelpers.java',
'src/org/mozilla/gecko/background/sync/helpers/PasswordHelpers.java',
'src/org/mozilla/gecko/background/sync/helpers/SessionTestHelper.java',
'src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessBeginDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessCreationDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFetchDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessFinishDelegate.java',
'src/org/mozilla/gecko/background/sync/helpers/SimpleSuccessStoreDelegate.java',
'src/org/mozilla/gecko/background/sync/TestAccountPickler.java',
'src/org/mozilla/gecko/background/sync/TestClientsStage.java',
'src/org/mozilla/gecko/background/sync/TestConfigurationMigrator.java',
'src/org/mozilla/gecko/background/sync/TestResetting.java',
'src/org/mozilla/gecko/background/sync/TestSendTabData.java',
'src/org/mozilla/gecko/background/sync/TestStoreTracking.java',
'src/org/mozilla/gecko/background/sync/TestSyncAccounts.java',
'src/org/mozilla/gecko/background/sync/TestSyncAuthenticatorService.java',
'src/org/mozilla/gecko/background/sync/TestSyncConfiguration.java',
'src/org/mozilla/gecko/background/sync/TestTabsRecord.java',
'src/org/mozilla/gecko/background/sync/TestUpgradeRequired.java',
'src/org/mozilla/gecko/background/sync/TestWebURLFinder.java',
'src/org/mozilla/gecko/background/telemetry/TestTelemetryRecorder.java',
'src/org/mozilla/gecko/background/testhelpers/BaseMockServerSyncStage.java',
'src/org/mozilla/gecko/background/testhelpers/CommandHelpers.java',
'src/org/mozilla/gecko/background/testhelpers/DefaultGlobalSessionCallback.java',
'src/org/mozilla/gecko/background/testhelpers/JPakeNumGeneratorFixed.java',
'src/org/mozilla/gecko/background/testhelpers/MockAbstractNonRepositorySyncStage.java',
'src/org/mozilla/gecko/background/testhelpers/MockClientsDatabaseAccessor.java',
'src/org/mozilla/gecko/background/testhelpers/MockClientsDataDelegate.java',
'src/org/mozilla/gecko/background/testhelpers/MockGlobalSession.java',
'src/org/mozilla/gecko/background/testhelpers/MockPrefsGlobalSession.java',
'src/org/mozilla/gecko/background/testhelpers/MockRecord.java',
'src/org/mozilla/gecko/background/testhelpers/MockServerSyncStage.java',
'src/org/mozilla/gecko/background/testhelpers/MockSharedPreferences.java',
'src/org/mozilla/gecko/background/testhelpers/StubDelegate.java',
'src/org/mozilla/gecko/background/testhelpers/WaitHelper.java',
'src/org/mozilla/gecko/background/testhelpers/WBORepository.java',
]

View File

@ -1,47 +1,47 @@
[DEFAULT]
subsuite = background
[src/common/TestAndroidLogWriters.java]
[src/common/TestBrowserContractHelpers.java]
[src/common/TestDateUtils.java]
[src/common/TestUtils.java]
[src/common/TestWaitHelper.java]
[src/db/TestAndroidBrowserBookmarksRepository.java]
[src/db/TestAndroidBrowserHistoryDataExtender.java]
[src/db/TestAndroidBrowserHistoryRepository.java]
[src/db/TestBookmarks.java]
[src/db/TestCachedSQLiteOpenHelper.java]
[src/db/TestClientsDatabase.java]
[src/db/TestClientsDatabaseAccessor.java]
[src/db/TestFennecTabsRepositorySession.java]
[src/db/TestFennecTabsStorage.java]
[src/db/TestFormHistoryRepositorySession.java]
[src/db/TestPasswordsRepository.java]
[src/fxa/TestBrowserIDKeyPairGeneration.java]
[src/fxa/authenticator/TestAccountPickler.java]
[src/healthreport/TestEnvironmentBuilder.java]
[src/healthreport/TestEnvironmentV1HashAppender.java]
[src/healthreport/TestHealthReportBroadcastService.java]
[src/healthreport/TestHealthReportDatabaseStorage.java]
[src/healthreport/TestHealthReportGenerator.java]
[src/healthreport/TestHealthReportProvider.java]
[src/healthreport/TestHealthReportSQLiteOpenHelper.java]
[src/healthreport/TestProfileInformationCache.java]
[src/healthreport/prune/TestHealthReportPruneService.java]
[src/healthreport/prune/TestPrunePolicyDatabaseStorage.java]
[src/healthreport/upload/TestAndroidSubmissionClient.java]
[src/healthreport/upload/TestHealthReportUploadService.java]
[src/nativecode/test/TestNativeCrypto.java]
[src/sync/TestAccountPickler.java]
[src/sync/TestClientsStage.java]
[src/sync/TestConfigurationMigrator.java]
[src/sync/TestResetting.java]
[src/sync/TestSendTabData.java]
[src/sync/TestStoreTracking.java]
[src/sync/TestSyncAccounts.java]
[src/sync/TestSyncAuthenticatorService.java]
[src/sync/TestSyncConfiguration.java]
[src/sync/TestTabsRecord.java]
[src/sync/TestUpgradeRequired.java]
[src/sync/TestWebURLFinder.java]
[src/telemetry/TestTelemetryRecorder.java]
[src/org/mozilla/gecko/background/common/TestAndroidLogWriters.java]
[src/org/mozilla/gecko/background/common/TestBrowserContractHelpers.java]
[src/org/mozilla/gecko/background/common/TestDateUtils.java]
[src/org/mozilla/gecko/background/common/TestUtils.java]
[src/org/mozilla/gecko/background/common/TestWaitHelper.java]
[src/org/mozilla/gecko/background/db/TestAndroidBrowserBookmarksRepository.java]
[src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryDataExtender.java]
[src/org/mozilla/gecko/background/db/TestAndroidBrowserHistoryRepository.java]
[src/org/mozilla/gecko/background/db/TestBookmarks.java]
[src/org/mozilla/gecko/background/db/TestCachedSQLiteOpenHelper.java]
[src/org/mozilla/gecko/background/db/TestClientsDatabase.java]
[src/org/mozilla/gecko/background/db/TestClientsDatabaseAccessor.java]
[src/org/mozilla/gecko/background/db/TestFennecTabsRepositorySession.java]
[src/org/mozilla/gecko/background/db/TestFennecTabsStorage.java]
[src/org/mozilla/gecko/background/db/TestFormHistoryRepositorySession.java]
[src/org/mozilla/gecko/background/db/TestPasswordsRepository.java]
[src/org/mozilla/gecko/background/fxa/TestBrowserIDKeyPairGeneration.java]
[src/org/mozilla/gecko/background/fxa/authenticator/TestAccountPickler.java]
[src/org/mozilla/gecko/background/healthreport/TestEnvironmentBuilder.java]
[src/org/mozilla/gecko/background/healthreport/TestEnvironmentV1HashAppender.java]
[src/org/mozilla/gecko/background/healthreport/TestHealthReportBroadcastService.java]
[src/org/mozilla/gecko/background/healthreport/TestHealthReportDatabaseStorage.java]
[src/org/mozilla/gecko/background/healthreport/TestHealthReportGenerator.java]
[src/org/mozilla/gecko/background/healthreport/TestHealthReportProvider.java]
[src/org/mozilla/gecko/background/healthreport/TestHealthReportSQLiteOpenHelper.java]
[src/org/mozilla/gecko/background/healthreport/TestProfileInformationCache.java]
[src/org/mozilla/gecko/background/healthreport/prune/TestHealthReportPruneService.java]
[src/org/mozilla/gecko/background/healthreport/prune/TestPrunePolicyDatabaseStorage.java]
[src/org/mozilla/gecko/background/healthreport/upload/TestAndroidSubmissionClient.java]
[src/org/mozilla/gecko/background/healthreport/upload/TestHealthReportUploadService.java]
[src/org/mozilla/gecko/background/nativecode/test/TestNativeCrypto.java]
[src/org/mozilla/gecko/background/sync/TestAccountPickler.java]
[src/org/mozilla/gecko/background/sync/TestClientsStage.java]
[src/org/mozilla/gecko/background/sync/TestConfigurationMigrator.java]
[src/org/mozilla/gecko/background/sync/TestResetting.java]
[src/org/mozilla/gecko/background/sync/TestSendTabData.java]
[src/org/mozilla/gecko/background/sync/TestStoreTracking.java]
[src/org/mozilla/gecko/background/sync/TestSyncAccounts.java]
[src/org/mozilla/gecko/background/sync/TestSyncAuthenticatorService.java]
[src/org/mozilla/gecko/background/sync/TestSyncConfiguration.java]
[src/org/mozilla/gecko/background/sync/TestTabsRecord.java]
[src/org/mozilla/gecko/background/sync/TestUpgradeRequired.java]
[src/org/mozilla/gecko/background/sync/TestWebURLFinder.java]
[src/org/mozilla/gecko/background/telemetry/TestTelemetryRecorder.java]

Some files were not shown because too many files have changed in this diff Show More