Merge fx-team to m-c. a=merge

This commit is contained in:
Ryan VanderMeulen 2015-07-27 10:39:37 -04:00
commit 4f09769d88
112 changed files with 2850 additions and 1179 deletions

View File

@ -711,6 +711,7 @@
@RESPATH@/components/nsUrlClassifierHashCompleter.js
@RESPATH@/components/nsUrlClassifierListManager.js
@RESPATH@/components/nsUrlClassifierLib.js
@RESPATH@/components/PrivateBrowsingTrackingProtectionWhitelist.js
@RESPATH@/components/url-classifier.xpt
; GNOME hooks

View File

@ -148,8 +148,13 @@ let wrapper = {
if (accountData.customizeSync) {
Services.prefs.setBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION, true);
delete accountData.customizeSync;
}
delete accountData.customizeSync;
// sessionTokenContext is erroneously sent by the content server.
// https://github.com/mozilla/fxa-content-server/issues/2766
// To avoid having the FxA storage manager not knowing what to do with
// it we delete it here.
delete accountData.sessionTokenContext;
// We need to confirm a relink - see shouldAllowRelink for more
let newAccountEmail = accountData.email;

View File

@ -116,8 +116,12 @@ let TrackingProtection = {
// Add the current host in the 'trackingprotection' consumer of
// the permission manager using a normalized URI. This effectively
// places this host on the tracking protection allowlist.
Services.perms.add(normalizedUrl,
"trackingprotection", Services.perms.ALLOW_ACTION);
if (PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser)) {
PrivateBrowsingUtils.addToTrackingAllowlist(normalizedUrl);
} else {
Services.perms.add(normalizedUrl,
"trackingprotection", Services.perms.ALLOW_ACTION);
}
// Telemetry for disable protection.
this.eventsHistogram.add(1);
@ -133,8 +137,11 @@ let TrackingProtection = {
"https://" + gBrowser.selectedBrowser.currentURI.hostPort,
null, null);
Services.perms.remove(normalizedUrl,
"trackingprotection");
if (PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser)) {
PrivateBrowsingUtils.removeFromTrackingAllowlist(normalizedUrl);
} else {
Services.perms.remove(normalizedUrl, "trackingprotection");
}
// Telemetry for enable protection.
this.eventsHistogram.add(2);

View File

@ -431,6 +431,10 @@ tags = trackingprotection
support-files =
trackingPage.html
benignPage.html
[browser_trackingUI_5.js]
tags = trackingprotection
support-files =
trackingPage.html
[browser_typeAheadFind.js]
skip-if = buildapp == 'mulet'
[browser_unknownContentType_title.js]

View File

@ -0,0 +1,122 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
// Test that sites added to the Tracking Protection whitelist in private
// browsing mode don't persist once the private browsing window closes.
const PB_PREF = "privacy.trackingprotection.pbmode.enabled";
const TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/test/general/trackingPage.html";
let TrackingProtection = null;
let browser = null;
let {UrlClassifierTestUtils} = Cu.import("resource://testing-common/UrlClassifierTestUtils.jsm", {});
registerCleanupFunction(function() {
TrackingProtection = browser = null;
UrlClassifierTestUtils.cleanupTestTrackers();
});
function hidden(sel) {
let win = browser.ownerGlobal;
let el = win.document.querySelector(sel);
let display = win.getComputedStyle(el).getPropertyValue("display", null);
return display === "none";
}
function clickButton(sel) {
let win = browser.ownerGlobal;
let el = win.document.querySelector(sel);
el.doCommand();
}
function testTrackingPage(window) {
info("Tracking content must be blocked");
ok(!TrackingProtection.container.hidden, "The container is visible");
is(TrackingProtection.content.getAttribute("state"), "blocked-tracking-content",
'content: state="blocked-tracking-content"');
is(TrackingProtection.icon.getAttribute("state"), "blocked-tracking-content",
'icon: state="blocked-tracking-content"');
ok(!hidden("#tracking-protection-icon"), "icon is visible");
ok(hidden("#tracking-action-block"), "blockButton is hidden");
ok(hidden("#tracking-action-unblock"), "unblockButton is hidden");
ok(!hidden("#tracking-action-unblock-private"), "unblockButtonPrivate is visible");
// Make sure that the blocked tracking elements message appears
ok(hidden("#tracking-not-detected"), "labelNoTracking is hidden");
ok(hidden("#tracking-loaded"), "labelTrackingLoaded is hidden");
ok(!hidden("#tracking-blocked"), "labelTrackingBlocked is visible");
}
function testTrackingPageUnblocked() {
info("Tracking content must be white-listed and not blocked");
ok(!TrackingProtection.container.hidden, "The container is visible");
is(TrackingProtection.content.getAttribute("state"), "loaded-tracking-content",
'content: state="loaded-tracking-content"');
is(TrackingProtection.icon.getAttribute("state"), "loaded-tracking-content",
'icon: state="loaded-tracking-content"');
ok(!hidden("#tracking-protection-icon"), "icon is visible");
ok(!hidden("#tracking-action-block"), "blockButton is visible");
ok(hidden("#tracking-action-unblock"), "unblockButton is hidden");
// Make sure that the blocked tracking elements message appears
ok(hidden("#tracking-not-detected"), "labelNoTracking is hidden");
ok(!hidden("#tracking-loaded"), "labelTrackingLoaded is visible");
ok(hidden("#tracking-blocked"), "labelTrackingBlocked is hidden");
}
add_task(function* testExceptionAddition() {
yield UrlClassifierTestUtils.addTestTrackers();
let privateWin = yield promiseOpenAndLoadWindow({private: true}, true);
browser = privateWin.gBrowser;
let tab = browser.selectedTab = browser.addTab();
TrackingProtection = browser.ownerGlobal.TrackingProtection;
yield pushPrefs([PB_PREF, true]);
ok(TrackingProtection.enabled, "TP is enabled after setting the pref");
info("Load a test page containing tracking elements");
yield promiseTabLoadEvent(tab, TRACKING_PAGE);
testTrackingPage(tab.ownerDocument.defaultView);
info("Disable TP for the page (which reloads the page)");
let tabReloadPromise = promiseTabLoadEvent(tab);
clickButton("#tracking-action-unblock");
yield tabReloadPromise;
testTrackingPageUnblocked();
info("Test that the exception is remembered across tabs in the same private window");
tab = browser.selectedTab = browser.addTab();
info("Load a test page containing tracking elements");
yield promiseTabLoadEvent(tab, TRACKING_PAGE);
testTrackingPageUnblocked();
yield promiseWindowClosed(privateWin);
});
add_task(function* testExceptionPersistence() {
info("Open another private browsing window");
let privateWin = yield promiseOpenAndLoadWindow({private: true}, true);
browser = privateWin.gBrowser;
let tab = browser.selectedTab = browser.addTab();
TrackingProtection = browser.ownerGlobal.TrackingProtection;
ok(TrackingProtection.enabled, "TP is still enabled");
info("Load a test page containing tracking elements");
yield promiseTabLoadEvent(tab, TRACKING_PAGE);
testTrackingPage(tab.ownerDocument.defaultView);
info("Disable TP for the page (which reloads the page)");
let tabReloadPromise = promiseTabLoadEvent(tab);
clickButton("#tracking-action-unblock");
yield tabReloadPromise;
testTrackingPageUnblocked();
privateWin.close();
});

View File

@ -957,9 +957,8 @@ const CustomizableWidgets = [
type: "custom",
label: "loop-call-button3.label",
tooltiptext: "loop-call-button3.tooltiptext",
privateBrowsingTooltiptext: "loop-call-button3-pb.tooltiptext",
defaultArea: CustomizableUI.AREA_NAVBAR,
// Not in private browsing, see bug 1108187.
showInPrivateBrowsing: false,
introducedInVersion: 4,
onBuild: function(aDocument) {
// If we're not supposed to see the button, return zip.
@ -967,13 +966,21 @@ const CustomizableWidgets = [
return null;
}
let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(aDocument.defaultView);
let node = aDocument.createElementNS(kNSXUL, "toolbarbutton");
node.setAttribute("id", this.id);
node.classList.add("toolbarbutton-1");
node.classList.add("chromeclass-toolbar-additional");
node.classList.add("badged-button");
node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label"));
node.setAttribute("tooltiptext", CustomizableUI.getLocalizedProperty(this, "tooltiptext"));
if (isWindowPrivate)
node.setAttribute("disabled", "true");
let tooltiptext = isWindowPrivate ?
CustomizableUI.getLocalizedProperty(this, "privateBrowsingTooltiptext",
[CustomizableUI.getLocalizedProperty(this, "label")]) :
CustomizableUI.getLocalizedProperty(this, "tooltiptext");
node.setAttribute("tooltiptext", tooltiptext);
node.setAttribute("removable", "true");
node.addEventListener("command", function(event) {
aDocument.defaultView.LoopUI.togglePanel(event);

View File

@ -119,7 +119,7 @@ function configureFxAccountIdentity() {
let storageManager = new MockFxaStorageManager();
// and init storage with our user.
storageManager.initialize(user);
return new AccountState(this, storageManager);
return new AccountState(storageManager);
},
getCertificate(data, keyPair, mustBeValidUntil) {
this.cert = {

View File

@ -570,13 +570,15 @@ loop.conversationViews = (function(mozL10n) {
var OngoingConversationView = React.createClass({displayName: "OngoingConversationView",
mixins: [
loop.store.StoreMixin("conversationStore"),
sharedMixins.MediaSetupMixin
],
propTypes: {
// local
audio: React.PropTypes.object,
// We pass conversationStore here rather than use the mixin, to allow
// easy configurability for the ui-showcase.
conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
// The poster URLs are for UI-showcase testing and development.
localPosterUrl: React.PropTypes.string,
@ -597,7 +599,17 @@ loop.conversationViews = (function(mozL10n) {
},
getInitialState: function() {
return this.getStoreState();
return this.props.conversationStore.getStoreState();
},
componentWillMount: function() {
this.props.conversationStore.on("change", function() {
this.setState(this.props.conversationStore.getStoreState());
}, this);
},
componentWillUnmount: function() {
this.props.conversationStore.off("change", null, this);
},
componentDidMount: function() {
@ -633,6 +645,30 @@ loop.conversationViews = (function(mozL10n) {
}));
},
/**
* Should we render a visual cue to the user (e.g. a spinner) that a local
* stream is on its way from the camera?
*
* @returns {boolean}
* @private
*/
_isLocalLoading: function () {
return !this.state.localSrcVideoObject && !this.props.localPosterUrl;
},
/**
* Should we render a visual cue to the user (e.g. a spinner) that a remote
* stream is on its way from the other user?
*
* @returns {boolean}
* @private
*/
_isRemoteLoading: function() {
return !!(!this.state.remoteSrcVideoObject &&
!this.props.remotePosterUrl &&
!this.state.mediaConnected);
},
shouldRenderRemoteVideo: function() {
if (this.props.mediaConnected) {
// If remote video is not enabled, we're muted, so we'll show an avatar
@ -646,41 +682,32 @@ loop.conversationViews = (function(mozL10n) {
},
render: function() {
var localStreamClasses = React.addons.classSet({
local: true,
"local-stream": true,
"local-stream-audio": !this.props.video.enabled
});
return (
React.createElement("div", {className: "video-layout-wrapper"},
React.createElement("div", {className: "conversation"},
React.createElement("div", {className: "media nested"},
React.createElement("div", {className: "video_wrapper remote_wrapper"},
React.createElement("div", {className: "video_inner remote focus-stream"},
React.createElement(sharedViews.MediaView, {displayAvatar: !this.shouldRenderRemoteVideo(),
isLoading: false,
mediaType: "remote",
posterUrl: this.props.remotePosterUrl,
srcVideoObject: this.state.remoteSrcVideoObject})
)
),
React.createElement("div", {className: localStreamClasses},
React.createElement(sharedViews.MediaView, {displayAvatar: !this.props.video.enabled,
isLoading: false,
mediaType: "local",
posterUrl: this.props.localPosterUrl,
srcVideoObject: this.state.localSrcVideoObject})
)
),
React.createElement(loop.shared.views.ConversationToolbar, {
audio: this.props.audio,
dispatcher: this.props.dispatcher,
edit: { visible: false, enabled: false},
hangup: this.hangup,
publishStream: this.publishStream,
video: this.props.video})
)
React.createElement("div", {className: "desktop-call-wrapper"},
React.createElement(sharedViews.MediaLayoutView, {
dispatcher: this.props.dispatcher,
displayScreenShare: false,
isLocalLoading: this._isLocalLoading(),
isRemoteLoading: this._isRemoteLoading(),
isScreenShareLoading: false,
localPosterUrl: this.props.localPosterUrl,
localSrcVideoObject: this.state.localSrcVideoObject,
localVideoMuted: !this.props.video.enabled,
matchMedia: this.state.matchMedia || window.matchMedia.bind(window),
remotePosterUrl: this.props.remotePosterUrl,
remoteSrcVideoObject: this.state.remoteSrcVideoObject,
renderRemoteVideo: this.shouldRenderRemoteVideo(),
screenSharePosterUrl: null,
screenShareVideoObject: this.state.screenShareVideoObject,
showContextRoomName: false,
useDesktopPaths: true}),
React.createElement(loop.shared.views.ConversationToolbar, {
audio: this.props.audio,
dispatcher: this.props.dispatcher,
edit: { visible: false, enabled: false},
hangup: this.hangup,
publishStream: this.publishStream,
video: this.props.video})
)
);
}
@ -778,6 +805,7 @@ loop.conversationViews = (function(mozL10n) {
case CALL_STATES.ONGOING: {
return (React.createElement(OngoingConversationView, {
audio: {enabled: !this.state.audioMuted},
conversationStore: this.getStore(),
dispatcher: this.props.dispatcher,
mediaConnected: this.state.mediaConnected,
remoteSrcVideoObject: this.state.remoteSrcVideoObject,

View File

@ -570,13 +570,15 @@ loop.conversationViews = (function(mozL10n) {
var OngoingConversationView = React.createClass({
mixins: [
loop.store.StoreMixin("conversationStore"),
sharedMixins.MediaSetupMixin
],
propTypes: {
// local
audio: React.PropTypes.object,
// We pass conversationStore here rather than use the mixin, to allow
// easy configurability for the ui-showcase.
conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
// The poster URLs are for UI-showcase testing and development.
localPosterUrl: React.PropTypes.string,
@ -597,7 +599,17 @@ loop.conversationViews = (function(mozL10n) {
},
getInitialState: function() {
return this.getStoreState();
return this.props.conversationStore.getStoreState();
},
componentWillMount: function() {
this.props.conversationStore.on("change", function() {
this.setState(this.props.conversationStore.getStoreState());
}, this);
},
componentWillUnmount: function() {
this.props.conversationStore.off("change", null, this);
},
componentDidMount: function() {
@ -633,6 +645,30 @@ loop.conversationViews = (function(mozL10n) {
}));
},
/**
* Should we render a visual cue to the user (e.g. a spinner) that a local
* stream is on its way from the camera?
*
* @returns {boolean}
* @private
*/
_isLocalLoading: function () {
return !this.state.localSrcVideoObject && !this.props.localPosterUrl;
},
/**
* Should we render a visual cue to the user (e.g. a spinner) that a remote
* stream is on its way from the other user?
*
* @returns {boolean}
* @private
*/
_isRemoteLoading: function() {
return !!(!this.state.remoteSrcVideoObject &&
!this.props.remotePosterUrl &&
!this.state.mediaConnected);
},
shouldRenderRemoteVideo: function() {
if (this.props.mediaConnected) {
// If remote video is not enabled, we're muted, so we'll show an avatar
@ -646,41 +682,32 @@ loop.conversationViews = (function(mozL10n) {
},
render: function() {
var localStreamClasses = React.addons.classSet({
local: true,
"local-stream": true,
"local-stream-audio": !this.props.video.enabled
});
return (
<div className="video-layout-wrapper">
<div className="conversation">
<div className="media nested">
<div className="video_wrapper remote_wrapper">
<div className="video_inner remote focus-stream">
<sharedViews.MediaView displayAvatar={!this.shouldRenderRemoteVideo()}
isLoading={false}
mediaType="remote"
posterUrl={this.props.remotePosterUrl}
srcVideoObject={this.state.remoteSrcVideoObject} />
</div>
</div>
<div className={localStreamClasses}>
<sharedViews.MediaView displayAvatar={!this.props.video.enabled}
isLoading={false}
mediaType="local"
posterUrl={this.props.localPosterUrl}
srcVideoObject={this.state.localSrcVideoObject} />
</div>
</div>
<loop.shared.views.ConversationToolbar
audio={this.props.audio}
dispatcher={this.props.dispatcher}
edit={{ visible: false, enabled: false }}
hangup={this.hangup}
publishStream={this.publishStream}
video={this.props.video} />
</div>
<div className="desktop-call-wrapper">
<sharedViews.MediaLayoutView
dispatcher={this.props.dispatcher}
displayScreenShare={false}
isLocalLoading={this._isLocalLoading()}
isRemoteLoading={this._isRemoteLoading()}
isScreenShareLoading={false}
localPosterUrl={this.props.localPosterUrl}
localSrcVideoObject={this.state.localSrcVideoObject}
localVideoMuted={!this.props.video.enabled}
matchMedia={this.state.matchMedia || window.matchMedia.bind(window)}
remotePosterUrl={this.props.remotePosterUrl}
remoteSrcVideoObject={this.state.remoteSrcVideoObject}
renderRemoteVideo={this.shouldRenderRemoteVideo()}
screenSharePosterUrl={null}
screenShareVideoObject={this.state.screenShareVideoObject}
showContextRoomName={false}
useDesktopPaths={true} />
<loop.shared.views.ConversationToolbar
audio={this.props.audio}
dispatcher={this.props.dispatcher}
edit={{ visible: false, enabled: false }}
hangup={this.hangup}
publishStream={this.publishStream}
video={this.props.video} />
</div>
);
}
@ -778,6 +805,7 @@ loop.conversationViews = (function(mozL10n) {
case CALL_STATES.ONGOING: {
return (<OngoingConversationView
audio={{enabled: !this.state.audioMuted}}
conversationStore={this.getStore()}
dispatcher={this.props.dispatcher}
mediaConnected={this.state.mediaConnected}
remoteSrcVideoObject={this.state.remoteSrcVideoObject}

View File

@ -665,7 +665,7 @@ loop.roomViews = (function(mozL10n) {
* @returns {boolean}
* @private
*/
_shouldRenderLocalLoading: function () {
_isLocalLoading: function () {
return this.state.roomState === ROOM_STATES.MEDIA_WAIT &&
!this.state.localSrcVideoObject;
},
@ -677,7 +677,7 @@ loop.roomViews = (function(mozL10n) {
* @returns {boolean}
* @private
*/
_shouldRenderRemoteLoading: function() {
_isRemoteLoading: function() {
return !!(this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS &&
!this.state.remoteSrcVideoObject &&
!this.state.mediaConnected);
@ -741,63 +741,54 @@ loop.roomViews = (function(mozL10n) {
return null;
}
default: {
return (
React.createElement("div", {className: "room-conversation-wrapper"},
React.createElement("div", {className: "video-layout-wrapper"},
React.createElement("div", {className: "conversation room-conversation"},
React.createElement("div", {className: "media nested"},
React.createElement(DesktopRoomInvitationView, {
dispatcher: this.props.dispatcher,
error: this.state.error,
mozLoop: this.props.mozLoop,
onAddContextClick: this.handleAddContextClick,
onEditContextClose: this.handleEditContextClose,
roomData: roomData,
savingContext: this.state.savingContext,
show: shouldRenderInvitationOverlay,
showEditContext: shouldRenderInvitationOverlay && shouldRenderEditContextView,
socialShareProviders: this.state.socialShareProviders}),
React.createElement("div", {className: "video_wrapper remote_wrapper"},
React.createElement("div", {className: "video_inner remote focus-stream"},
React.createElement(sharedViews.MediaView, {displayAvatar: !this.shouldRenderRemoteVideo(),
isLoading: this._shouldRenderRemoteLoading(),
mediaType: "remote",
posterUrl: this.props.remotePosterUrl,
srcVideoObject: this.state.remoteSrcVideoObject})
)
),
React.createElement("div", {className: localStreamClasses},
React.createElement(sharedViews.MediaView, {displayAvatar: this.state.videoMuted,
isLoading: this._shouldRenderLocalLoading(),
mediaType: "local",
posterUrl: this.props.localPosterUrl,
srcVideoObject: this.state.localSrcVideoObject})
),
React.createElement(DesktopRoomEditContextView, {
dispatcher: this.props.dispatcher,
error: this.state.error,
mozLoop: this.props.mozLoop,
onClose: this.handleEditContextClose,
roomData: roomData,
savingContext: this.state.savingContext,
show: !shouldRenderInvitationOverlay && shouldRenderEditContextView})
),
React.createElement(sharedViews.ConversationToolbar, {
audio: {enabled: !this.state.audioMuted, visible: true},
dispatcher: this.props.dispatcher,
edit: { visible: this.state.contextEnabled, enabled: !this.state.showEditContext},
hangup: this.leaveRoom,
onEditClick: this.handleEditContextClick,
publishStream: this.publishStream,
screenShare: screenShareData,
video: {enabled: !this.state.videoMuted, visible: true}})
)
),
React.createElement(sharedViews.chat.TextChatView, {
React.createElement("div", {className: "room-conversation-wrapper desktop-room-wrapper"},
React.createElement(sharedViews.MediaLayoutView, {
dispatcher: this.props.dispatcher,
showRoomName: false,
useDesktopPaths: true})
displayScreenShare: false,
isLocalLoading: this._isLocalLoading(),
isRemoteLoading: this._isRemoteLoading(),
isScreenShareLoading: false,
localPosterUrl: this.props.localPosterUrl,
localSrcVideoObject: this.state.localSrcVideoObject,
localVideoMuted: this.state.videoMuted,
matchMedia: this.state.matchMedia || window.matchMedia.bind(window),
remotePosterUrl: this.props.remotePosterUrl,
remoteSrcVideoObject: this.state.remoteSrcVideoObject,
renderRemoteVideo: this.shouldRenderRemoteVideo(),
screenSharePosterUrl: null,
screenShareVideoObject: this.state.screenShareVideoObject,
showContextRoomName: false,
useDesktopPaths: true},
React.createElement(DesktopRoomInvitationView, {
dispatcher: this.props.dispatcher,
error: this.state.error,
mozLoop: this.props.mozLoop,
onAddContextClick: this.handleAddContextClick,
onEditContextClose: this.handleEditContextClose,
roomData: roomData,
savingContext: this.state.savingContext,
show: shouldRenderInvitationOverlay,
showEditContext: shouldRenderInvitationOverlay && shouldRenderEditContextView,
socialShareProviders: this.state.socialShareProviders}),
React.createElement(DesktopRoomEditContextView, {
dispatcher: this.props.dispatcher,
error: this.state.error,
mozLoop: this.props.mozLoop,
onClose: this.handleEditContextClose,
roomData: roomData,
savingContext: this.state.savingContext,
show: !shouldRenderInvitationOverlay && shouldRenderEditContextView})
),
React.createElement(sharedViews.ConversationToolbar, {
audio: {enabled: !this.state.audioMuted, visible: true},
dispatcher: this.props.dispatcher,
edit: { visible: this.state.contextEnabled, enabled: !this.state.showEditContext},
hangup: this.leaveRoom,
onEditClick: this.handleEditContextClick,
publishStream: this.publishStream,
screenShare: screenShareData,
video: {enabled: !this.state.videoMuted, visible: true}})
)
);
}

View File

@ -665,7 +665,7 @@ loop.roomViews = (function(mozL10n) {
* @returns {boolean}
* @private
*/
_shouldRenderLocalLoading: function () {
_isLocalLoading: function () {
return this.state.roomState === ROOM_STATES.MEDIA_WAIT &&
!this.state.localSrcVideoObject;
},
@ -677,7 +677,7 @@ loop.roomViews = (function(mozL10n) {
* @returns {boolean}
* @private
*/
_shouldRenderRemoteLoading: function() {
_isRemoteLoading: function() {
return !!(this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS &&
!this.state.remoteSrcVideoObject &&
!this.state.mediaConnected);
@ -741,63 +741,54 @@ loop.roomViews = (function(mozL10n) {
return null;
}
default: {
return (
<div className="room-conversation-wrapper">
<div className="video-layout-wrapper">
<div className="conversation room-conversation">
<div className="media nested">
<DesktopRoomInvitationView
dispatcher={this.props.dispatcher}
error={this.state.error}
mozLoop={this.props.mozLoop}
onAddContextClick={this.handleAddContextClick}
onEditContextClose={this.handleEditContextClose}
roomData={roomData}
savingContext={this.state.savingContext}
show={shouldRenderInvitationOverlay}
showEditContext={shouldRenderInvitationOverlay && shouldRenderEditContextView}
socialShareProviders={this.state.socialShareProviders} />
<div className="video_wrapper remote_wrapper">
<div className="video_inner remote focus-stream">
<sharedViews.MediaView displayAvatar={!this.shouldRenderRemoteVideo()}
isLoading={this._shouldRenderRemoteLoading()}
mediaType="remote"
posterUrl={this.props.remotePosterUrl}
srcVideoObject={this.state.remoteSrcVideoObject} />
</div>
</div>
<div className={localStreamClasses}>
<sharedViews.MediaView displayAvatar={this.state.videoMuted}
isLoading={this._shouldRenderLocalLoading()}
mediaType="local"
posterUrl={this.props.localPosterUrl}
srcVideoObject={this.state.localSrcVideoObject} />
</div>
<DesktopRoomEditContextView
dispatcher={this.props.dispatcher}
error={this.state.error}
mozLoop={this.props.mozLoop}
onClose={this.handleEditContextClose}
roomData={roomData}
savingContext={this.state.savingContext}
show={!shouldRenderInvitationOverlay && shouldRenderEditContextView} />
</div>
<sharedViews.ConversationToolbar
audio={{enabled: !this.state.audioMuted, visible: true}}
dispatcher={this.props.dispatcher}
edit={{ visible: this.state.contextEnabled, enabled: !this.state.showEditContext }}
hangup={this.leaveRoom}
onEditClick={this.handleEditContextClick}
publishStream={this.publishStream}
screenShare={screenShareData}
video={{enabled: !this.state.videoMuted, visible: true}} />
</div>
</div>
<sharedViews.chat.TextChatView
<div className="room-conversation-wrapper desktop-room-wrapper">
<sharedViews.MediaLayoutView
dispatcher={this.props.dispatcher}
showRoomName={false}
useDesktopPaths={true} />
displayScreenShare={false}
isLocalLoading={this._isLocalLoading()}
isRemoteLoading={this._isRemoteLoading()}
isScreenShareLoading={false}
localPosterUrl={this.props.localPosterUrl}
localSrcVideoObject={this.state.localSrcVideoObject}
localVideoMuted={this.state.videoMuted}
matchMedia={this.state.matchMedia || window.matchMedia.bind(window)}
remotePosterUrl={this.props.remotePosterUrl}
remoteSrcVideoObject={this.state.remoteSrcVideoObject}
renderRemoteVideo={this.shouldRenderRemoteVideo()}
screenSharePosterUrl={null}
screenShareVideoObject={this.state.screenShareVideoObject}
showContextRoomName={false}
useDesktopPaths={true}>
<DesktopRoomInvitationView
dispatcher={this.props.dispatcher}
error={this.state.error}
mozLoop={this.props.mozLoop}
onAddContextClick={this.handleAddContextClick}
onEditContextClose={this.handleEditContextClose}
roomData={roomData}
savingContext={this.state.savingContext}
show={shouldRenderInvitationOverlay}
showEditContext={shouldRenderInvitationOverlay && shouldRenderEditContextView}
socialShareProviders={this.state.socialShareProviders} />
<DesktopRoomEditContextView
dispatcher={this.props.dispatcher}
error={this.state.error}
mozLoop={this.props.mozLoop}
onClose={this.handleEditContextClose}
roomData={roomData}
savingContext={this.state.savingContext}
show={!shouldRenderInvitationOverlay && shouldRenderEditContextView} />
</sharedViews.MediaLayoutView>
<sharedViews.ConversationToolbar
audio={{enabled: !this.state.audioMuted, visible: true}}
dispatcher={this.props.dispatcher}
edit={{ visible: this.state.contextEnabled, enabled: !this.state.showEditContext }}
hangup={this.leaveRoom}
onEditClick={this.handleEditContextClick}
publishStream={this.publishStream}
screenShare={screenShareData}
video={{enabled: !this.state.videoMuted, visible: true}} />
</div>
);
}

View File

@ -504,28 +504,6 @@
text-align: center;
}
.fx-embedded .local-stream {
position: absolute;
right: 3px;
bottom: 5px;
/* next two lines are workaround for lack of object-fit; see bug 1020445 */
max-width: 140px;
width: 30%;
height: 28%;
max-height: 105px;
}
.fx-embedded .local-stream.room-preview {
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
height: 100%;
width: 100%;
max-width: none;
max-height: none;
}
.conversation .media.nested .focus-stream {
display: inline-block;
position: absolute; /* workaround for lack of object-fit; see bug 1020445 */
@ -592,15 +570,11 @@
z-index: 1;
}
.remote .avatar {
.remote > .avatar {
/* make visually distinct from local avatar */
opacity: 0.25;
}
.fx-embedded .media.nested {
min-height: 200px;
}
.fx-embedded-call-identifier {
display: inline;
width: 100%;
@ -675,7 +649,9 @@
* */
html, .fx-embedded, #main,
.video-layout-wrapper,
.conversation {
.conversation,
.desktop-call-wrapper,
.desktop-room-wrapper {
height: 100%;
}
@ -935,7 +911,6 @@ body[platform="win"] .share-service-dropdown.overflow > .dropdown-menu-item {
border-top: 2px solid #444;
border-bottom: 2px solid #444;
padding: .5rem;
max-height: 400px;
position: absolute;
left: 0;
bottom: 0;
@ -951,7 +926,7 @@ body[platform="win"] .share-service-dropdown.overflow > .dropdown-menu-item {
overflow-x: hidden;
overflow-y: auto;
/* Make the context view float atop the video elements. */
z-index: 2;
z-index: 3;
}
.room-invitation-overlay .room-context {
@ -1087,12 +1062,12 @@ html[dir="rtl"] .room-context-btn-close {
.standalone-room-wrapper > .media-layout {
/* 50px is the header, 64px for toolbar, 3em is the footer. */
height: calc(100% - 50px - 64px - 3em);
margin: 0 10px;
}
.media-layout > .media-wrapper {
display: flex;
flex-flow: column wrap;
margin: 0 10px;
height: 100%;
}
@ -1139,6 +1114,14 @@ html[dir="rtl"] .room-context-btn-close {
height: calc(100% - 300px);
}
.desktop-call-wrapper > .media-layout > .media-wrapper > .text-chat-view,
.desktop-room-wrapper > .media-layout > .media-wrapper > .text-chat-view {
/* Account for height of .conversation-toolbar on desktop */
/* When we change the toolbar in bug 1184559 we can remove this. */
margin-top: 26px;
height: calc(100% - 150px - 26px);
}
/* Temporarily slaved from .media-wrapper until we use it in more places
to avoid affecting the conversation window on desktop. */
.media-wrapper > .text-chat-view > .text-chat-entries {
@ -1204,7 +1187,7 @@ html[dir="rtl"] .room-context-btn-close {
/* Temporarily slaved from .media-wrapper until we use it in more places
to avoid affecting the conversation window on desktop. */
.media-wrapper > .text-chat-view > .text-chat-entries {
.text-chat-view > .text-chat-entries {
/* 40px is the height of .text-chat-box. */
height: calc(100% - 40px);
width: 100%;
@ -1215,20 +1198,26 @@ html[dir="rtl"] .room-context-btn-close {
height: 100%;
}
.media-wrapper > .local {
.media-wrapper > .focus-stream > .local {
/* Position over the remote video */
position: absolute;
/* Make sure its on top */
z-index: 1001;
z-index: 2;
margin: 3px;
right: 0;
/* 29px is (30% of 50px high header) + (height toolbar (38px) +
height footer (25px) - height header (50px)) */
bottom: calc(30% + 29px);
bottom: 0;
width: 120px;
height: 120px;
}
.standalone-room-wrapper > .media-layout > .media-wrapper > .local {
/* Add 10px for the margin on standalone */
right: 10px;
}
html[dir="rtl"] .media-wrapper > .local {
right: auto;
left: 0;
@ -1247,6 +1236,15 @@ html[dir="rtl"] .room-context-btn-close {
height: 30%;
}
.desktop-call-wrapper > .media-layout > .media-wrapper > .text-chat-view,
.desktop-room-wrapper > .media-layout > .media-wrapper > .text-chat-view {
/* When we change the toolbar in bug 1184559 we can remove this. */
/* Reset back to 0 for .conversation-toolbar override on desktop */
margin-top: 0;
/* This is temp, to echo the .media-wrapper > .text-chat-view above */
height: 30%;
}
.media-wrapper.receiving-screen-share > .screen {
order: 1;
}
@ -1288,6 +1286,47 @@ html[dir="rtl"] .room-context-btn-close {
}
}
/* e.g. very narrow widths similar to conversation window */
@media screen and (max-width:300px) {
.media-layout > .media-wrapper {
flex-flow: column nowrap;
}
.media-wrapper > .focus-stream > .local {
position: absolute;
right: 0;
/* 30% is the height of the text chat. As we have a margin,
we don't need to worry about any offset for a border */
bottom: 0;
margin: 3px;
object-fit: contain;
/* These make the avatar look reasonable and the local
video not too big */
width: 25%;
height: 25%;
}
.media-wrapper:not(.showing-remote-streams) > .focus-stream > .no-video {
display: none;
}
.media-wrapper:not(.showing-remote-streams) > .focus-stream > .local {
position: relative;
margin: 0;
right: auto;
left: auto;
bottom: auto;
width: 100%;
height: 100%;
background-color: black;
}
.media-wrapper > .focus-stream {
flex: 1 1 auto;
height: auto;
}
}
.standalone > #main > .room-conversation-wrapper > .media-layout > .conversation-toolbar {
border: none;
}
@ -1415,37 +1454,12 @@ html[dir="rtl"] .standalone .room-conversation-wrapper .room-inner-info-area {
height: auto;
}
/* Text chat in rooms styles */
.fx-embedded .room-conversation-wrapper {
display: flex;
flex-flow: column nowrap;
}
.fx-embedded .video-layout-wrapper {
flex: 1 1 auto;
}
/* Text chat in styles */
.text-chat-view {
background: white;
}
.fx-embedded .text-chat-view {
flex: 1 0 auto;
display: flex;
flex-flow: column nowrap;
}
.fx-embedded .text-chat-entries {
flex: 1 1 auto;
max-height: 120px;
min-height: 60px;
}
.fx-embedded .text-chat-view > .text-chat-entries-empty {
display: none;
}
.text-chat-box {
flex: 0 0 auto;
max-height: 40px;
@ -1740,6 +1754,47 @@ html[dir="rtl"] .text-chat-entry.received .text-chat-arrow {
}
}
/* e.g. very narrow widths similar to conversation window */
@media screen and (max-width:300px) {
.text-chat-view {
flex: 0 0 auto;
display: flex;
flex-flow: column nowrap;
/* 120px max-height of .text-chat-entries plus 40px of .text-chat-box */
max-height: 160px;
/* 60px min-height of .text-chat-entries plus 40px of .text-chat-box */
min-height: 100px;
/* The !important is to override the values defined above which have more
specificity when we fix bug 1184559, we should be able to remove it,
but this should be tests first. */
height: auto !important;
}
.text-chat-entries {
/* The !important is to override the values defined above which have more
specificity when we fix bug 1184559, we should be able to remove it,
but this should be tests first. */
flex: 1 1 auto !important;
max-height: 120px;
min-height: 60px;
}
.text-chat-entries-empty.text-chat-disabled {
display: none;
}
/* When the text chat entries are not present, then hide the entries view
and just show the chat box. */
.text-chat-entries-empty {
max-height: 40px;
min-height: 40px;
}
.text-chat-entries-empty > .text-chat-entries {
display: none;
}
}
.self-view-hidden-message {
/* Not displayed by default; display is turned on elsewhere when the
* self-view is actually hidden.

View File

@ -580,21 +580,6 @@ loop.store.ActiveRoomStore = (function() {
* @param {sharedActions.ConnectionFailure} actionData
*/
connectionFailure: function(actionData) {
/**
* XXX This is a workaround for desktop machines that do not have a
* camera installed. As we don't yet have device enumeration, when
* we do, this can be removed (bug 1138851), and the sdk should handle it.
*/
if (this._isDesktop &&
actionData.reason === FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA &&
this.getStoreState().videoMuted === false) {
// We failed to publish with media, so due to the bug, we try again without
// video.
this.setStoreState({videoMuted: true});
this._sdkDriver.retryPublishWithoutVideo();
return;
}
var exitState = this._storeState.roomState === ROOM_STATES.FAILED ?
this._storeState.failureExitState : this._storeState.roomState;

View File

@ -146,21 +146,6 @@ loop.store = loop.store || {};
* @param {sharedActions.ConnectionFailure} actionData The action data.
*/
connectionFailure: function(actionData) {
/**
* XXX This is a workaround for desktop machines that do not have a
* camera installed. As we don't yet have device enumeration, when
* we do, this can be removed (bug 1138851), and the sdk should handle it.
*/
if (this._isDesktop &&
actionData.reason === FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA &&
this.getStoreState().videoMuted === false) {
// We failed to publish with media, so due to the bug, we try again without
// video.
this.setStoreState({videoMuted: true});
this.sdkDriver.retryPublishWithoutVideo();
return;
}
this._endSession();
this.setStoreState({
callState: CALL_STATES.TERMINATED,

View File

@ -61,15 +61,26 @@ loop.OTSdkDriver = (function() {
/**
* XXX This is a workaround for desktop machines that do not have a
* camera installed. As we don't yet have device enumeration, when
* we do, this can be removed (bug 1138851), and the sdk should handle it.
* camera installed. The SDK doesn't currently do use the new device
* enumeration apis, when it does (bug 1138851), we can drop this part.
*/
if (this._isDesktop && !window.MediaStreamTrack.getSources) {
if (this._isDesktop) {
// If there's no getSources function, the sdk defines its own and caches
// the result. So here we define the "normal" one which doesn't get cached, so
// we can change it later.
// the result. So here we define our own one which wraps around the
// real device enumeration api.
window.MediaStreamTrack.getSources = function(callback) {
callback([{kind: "audio"}, {kind: "video"}]);
navigator.mediaDevices.enumerateDevices().then(function(devices) {
var result = [];
devices.forEach(function(device) {
if (device.kind === "audioinput") {
result.push({kind: "audio"});
}
if (device.kind === "videoinput") {
result.push({kind: "video"});
}
});
callback(result);
});
};
}
};
@ -109,21 +120,13 @@ loop.OTSdkDriver = (function() {
this.sdk.on("exception", this._onOTException.bind(this));
// At this state we init the publisher, even though we might be waiting for
// the initial connect of the session. This saves time when setting up
// the media.
this._publishLocalStreams();
},
/**
* Internal function to publish a local stream.
* XXX This can be simplified when bug 1138851 is actioned.
*/
_publishLocalStreams: function() {
// We expect the local video to be muted automatically by the SDK. Hence
// we don't mute it manually here.
this._mockPublisherEl = document.createElement("div");
// At this state we init the publisher, even though we might be waiting for
// the initial connect of the session. This saves time when setting up
// the media.
this.publisher = this.sdk.initPublisher(this._mockPublisherEl,
_.extend(this._getDataChannelSettings, this._getCopyPublisherConfig));
@ -135,17 +138,6 @@ loop.OTSdkDriver = (function() {
this._onAccessDialogOpened.bind(this));
},
/**
* Forces the sdk into not using video, and starts publishing again.
* XXX This is part of the work around that will be removed by bug 1138851.
*/
retryPublishWithoutVideo: function() {
window.MediaStreamTrack.getSources = function(callback) {
callback([{kind: "audio"}]);
};
this._publishLocalStreams();
},
/**
* Handles the setMute action. Informs the published stream to mute
* or unmute audio as appropriate.

View File

@ -150,8 +150,7 @@ loop.shared.views.chat = (function(mozL10n) {
var lastTimestamp = 0;
var entriesClasses = React.addons.classSet({
"text-chat-entries": true,
"text-chat-entries-empty": !this.props.messageList.length
"text-chat-entries": true
});
return (
@ -382,7 +381,8 @@ loop.shared.views.chat = (function(mozL10n) {
var textChatViewClasses = React.addons.classSet({
"text-chat-view": true,
"text-chat-disabled": !this.state.textChatEnabled
"text-chat-disabled": !this.state.textChatEnabled,
"text-chat-entries-empty": !messageList.length
});
return (

View File

@ -150,8 +150,7 @@ loop.shared.views.chat = (function(mozL10n) {
var lastTimestamp = 0;
var entriesClasses = React.addons.classSet({
"text-chat-entries": true,
"text-chat-entries-empty": !this.props.messageList.length
"text-chat-entries": true
});
return (
@ -382,7 +381,8 @@ loop.shared.views.chat = (function(mozL10n) {
var textChatViewClasses = React.addons.classSet({
"text-chat-view": true,
"text-chat-disabled": !this.state.textChatEnabled
"text-chat-disabled": !this.state.textChatEnabled,
"text-chat-entries-empty": !messageList.length
});
return (

View File

@ -946,6 +946,7 @@ loop.shared.views = (function(_, mozL10n) {
var MediaLayoutView = React.createClass({displayName: "MediaLayoutView",
propTypes: {
children: React.PropTypes.node,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
displayScreenShare: React.PropTypes.bool.isRequired,
isLocalLoading: React.PropTypes.bool.isRequired,
@ -955,6 +956,9 @@ loop.shared.views = (function(_, mozL10n) {
localPosterUrl: React.PropTypes.string,
localSrcVideoObject: React.PropTypes.object,
localVideoMuted: React.PropTypes.bool.isRequired,
// Passing in matchMedia, allows it to be overriden for ui-showcase's
// benefit. We expect either the override or window.matchMedia.
matchMedia: React.PropTypes.func.isRequired,
remotePosterUrl: React.PropTypes.string,
remoteSrcVideoObject: React.PropTypes.object,
renderRemoteVideo: React.PropTypes.bool.isRequired,
@ -964,6 +968,60 @@ loop.shared.views = (function(_, mozL10n) {
useDesktopPaths: React.PropTypes.bool.isRequired
},
isLocalMediaAbsolutelyPositioned: function(matchMedia) {
if (!matchMedia) {
matchMedia = this.props.matchMedia;
}
return matchMedia &&
// The screen width is less than 640px and we are not screen sharing.
((matchMedia("screen and (max-width:640px)").matches &&
!this.props.displayScreenShare) ||
// or the screen width is less than 300px.
(matchMedia("screen and (max-width:300px)").matches));
},
getInitialState: function() {
return {
localMediaAboslutelyPositioned: this.isLocalMediaAbsolutelyPositioned()
};
},
componentWillReceiveProps: function(nextProps) {
// This is all for the ui-showcase's benefit.
if (this.props.matchMedia != nextProps.matchMedia) {
this.updateLocalMediaState(null, nextProps.matchMedia);
}
},
componentDidMount: function() {
window.addEventListener("resize", this.updateLocalMediaState);
},
componentWillUnmount: function() {
window.removeEventListener("resize", this.updateLocalMediaState);
},
updateLocalMediaState: function(event, matchMedia) {
var newState = this.isLocalMediaAbsolutelyPositioned(matchMedia);
if (this.state.localMediaAboslutelyPositioned != newState) {
this.setState({
localMediaAboslutelyPositioned: newState
});
}
},
renderLocalVideo: function() {
return (
React.createElement("div", {className: "local"},
React.createElement(MediaView, {displayAvatar: this.props.localVideoMuted,
isLoading: this.props.isLocalLoading,
mediaType: "local",
posterUrl: this.props.localPosterUrl,
srcVideoObject: this.props.localSrcVideoObject})
)
);
},
render: function() {
var remoteStreamClasses = React.addons.classSet({
"remote": true,
@ -979,7 +1037,9 @@ loop.shared.views = (function(_, mozL10n) {
"media-wrapper": true,
"receiving-screen-share": this.props.displayScreenShare,
"showing-local-streams": this.props.localSrcVideoObject ||
this.props.localPosterUrl
this.props.localPosterUrl,
"showing-remote-streams": this.props.remoteSrcVideoObject ||
this.props.remotePosterUrl || this.props.isRemoteLoading
});
return (
@ -993,7 +1053,10 @@ loop.shared.views = (function(_, mozL10n) {
isLoading: this.props.isRemoteLoading,
mediaType: "remote",
posterUrl: this.props.remotePosterUrl,
srcVideoObject: this.props.remoteSrcVideoObject})
srcVideoObject: this.props.remoteSrcVideoObject}),
this.state.localMediaAboslutelyPositioned ?
this.renderLocalVideo() : null,
this.props.children
),
React.createElement("div", {className: screenShareStreamClasses},
React.createElement(MediaView, {displayAvatar: false,
@ -1006,13 +1069,8 @@ loop.shared.views = (function(_, mozL10n) {
dispatcher: this.props.dispatcher,
showRoomName: this.props.showContextRoomName,
useDesktopPaths: false}),
React.createElement("div", {className: "local"},
React.createElement(MediaView, {displayAvatar: this.props.localVideoMuted,
isLoading: this.props.isLocalLoading,
mediaType: "local",
posterUrl: this.props.localPosterUrl,
srcVideoObject: this.props.localSrcVideoObject})
)
this.state.localMediaAboslutelyPositioned ?
null : this.renderLocalVideo()
)
)
);

View File

@ -946,6 +946,7 @@ loop.shared.views = (function(_, mozL10n) {
var MediaLayoutView = React.createClass({
propTypes: {
children: React.PropTypes.node,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
displayScreenShare: React.PropTypes.bool.isRequired,
isLocalLoading: React.PropTypes.bool.isRequired,
@ -955,6 +956,9 @@ loop.shared.views = (function(_, mozL10n) {
localPosterUrl: React.PropTypes.string,
localSrcVideoObject: React.PropTypes.object,
localVideoMuted: React.PropTypes.bool.isRequired,
// Passing in matchMedia, allows it to be overriden for ui-showcase's
// benefit. We expect either the override or window.matchMedia.
matchMedia: React.PropTypes.func.isRequired,
remotePosterUrl: React.PropTypes.string,
remoteSrcVideoObject: React.PropTypes.object,
renderRemoteVideo: React.PropTypes.bool.isRequired,
@ -964,6 +968,60 @@ loop.shared.views = (function(_, mozL10n) {
useDesktopPaths: React.PropTypes.bool.isRequired
},
isLocalMediaAbsolutelyPositioned: function(matchMedia) {
if (!matchMedia) {
matchMedia = this.props.matchMedia;
}
return matchMedia &&
// The screen width is less than 640px and we are not screen sharing.
((matchMedia("screen and (max-width:640px)").matches &&
!this.props.displayScreenShare) ||
// or the screen width is less than 300px.
(matchMedia("screen and (max-width:300px)").matches));
},
getInitialState: function() {
return {
localMediaAboslutelyPositioned: this.isLocalMediaAbsolutelyPositioned()
};
},
componentWillReceiveProps: function(nextProps) {
// This is all for the ui-showcase's benefit.
if (this.props.matchMedia != nextProps.matchMedia) {
this.updateLocalMediaState(null, nextProps.matchMedia);
}
},
componentDidMount: function() {
window.addEventListener("resize", this.updateLocalMediaState);
},
componentWillUnmount: function() {
window.removeEventListener("resize", this.updateLocalMediaState);
},
updateLocalMediaState: function(event, matchMedia) {
var newState = this.isLocalMediaAbsolutelyPositioned(matchMedia);
if (this.state.localMediaAboslutelyPositioned != newState) {
this.setState({
localMediaAboslutelyPositioned: newState
});
}
},
renderLocalVideo: function() {
return (
<div className="local">
<MediaView displayAvatar={this.props.localVideoMuted}
isLoading={this.props.isLocalLoading}
mediaType="local"
posterUrl={this.props.localPosterUrl}
srcVideoObject={this.props.localSrcVideoObject} />
</div>
);
},
render: function() {
var remoteStreamClasses = React.addons.classSet({
"remote": true,
@ -979,7 +1037,9 @@ loop.shared.views = (function(_, mozL10n) {
"media-wrapper": true,
"receiving-screen-share": this.props.displayScreenShare,
"showing-local-streams": this.props.localSrcVideoObject ||
this.props.localPosterUrl
this.props.localPosterUrl,
"showing-remote-streams": this.props.remoteSrcVideoObject ||
this.props.remotePosterUrl || this.props.isRemoteLoading
});
return (
@ -994,6 +1054,9 @@ loop.shared.views = (function(_, mozL10n) {
mediaType="remote"
posterUrl={this.props.remotePosterUrl}
srcVideoObject={this.props.remoteSrcVideoObject} />
{ this.state.localMediaAboslutelyPositioned ?
this.renderLocalVideo() : null }
{ this.props.children }
</div>
<div className={screenShareStreamClasses}>
<MediaView displayAvatar={false}
@ -1006,13 +1069,8 @@ loop.shared.views = (function(_, mozL10n) {
dispatcher={this.props.dispatcher}
showRoomName={this.props.showContextRoomName}
useDesktopPaths={false} />
<div className="local">
<MediaView displayAvatar={this.props.localVideoMuted}
isLoading={this.props.isLocalLoading}
mediaType="local"
posterUrl={this.props.localPosterUrl}
srcVideoObject={this.props.localSrcVideoObject} />
</div>
{ this.state.localMediaAboslutelyPositioned ?
null : this.renderLocalVideo() }
</div>
</div>
);

View File

@ -256,11 +256,12 @@ loop.standaloneRoomViews = (function(mozL10n) {
mixins: [
Backbone.Events,
sharedMixins.MediaSetupMixin,
sharedMixins.RoomsAudioMixin,
loop.store.StoreMixin("activeRoomStore")
sharedMixins.RoomsAudioMixin
],
propTypes: {
// We pass conversationStore here rather than use the mixin, to allow
// easy configurability for the ui-showcase.
activeRoomStore: React.PropTypes.oneOfType([
React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
@ -282,6 +283,16 @@ loop.standaloneRoomViews = (function(mozL10n) {
});
},
componentWillMount: function() {
this.props.activeRoomStore.on("change", function() {
this.setState(this.props.activeRoomStore.getStoreState());
}, this);
},
componentWillUnmount: function() {
this.props.activeRoomStore.off("change", null, this);
},
componentDidMount: function() {
// Adding a class to the document body element from here to ease styling it.
document.body.classList.add("is-standalone-room");
@ -429,7 +440,8 @@ loop.standaloneRoomViews = (function(mozL10n) {
*/
_isScreenShareLoading: function() {
return this.state.receivingScreenShare &&
!this.state.screenShareVideoObject;
!this.state.screenShareVideoObject &&
!this.props.screenSharePosterUrl;
},
render: function() {
@ -456,6 +468,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
localPosterUrl: this.props.localPosterUrl,
localSrcVideoObject: this.state.localSrcVideoObject,
localVideoMuted: this.state.videoMuted,
matchMedia: this.state.matchMedia || window.matchMedia.bind(window),
remotePosterUrl: this.props.remotePosterUrl,
remoteSrcVideoObject: this.state.remoteSrcVideoObject,
renderRemoteVideo: this.shouldRenderRemoteVideo(),

View File

@ -256,11 +256,12 @@ loop.standaloneRoomViews = (function(mozL10n) {
mixins: [
Backbone.Events,
sharedMixins.MediaSetupMixin,
sharedMixins.RoomsAudioMixin,
loop.store.StoreMixin("activeRoomStore")
sharedMixins.RoomsAudioMixin
],
propTypes: {
// We pass conversationStore here rather than use the mixin, to allow
// easy configurability for the ui-showcase.
activeRoomStore: React.PropTypes.oneOfType([
React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
@ -282,6 +283,16 @@ loop.standaloneRoomViews = (function(mozL10n) {
});
},
componentWillMount: function() {
this.props.activeRoomStore.on("change", function() {
this.setState(this.props.activeRoomStore.getStoreState());
}, this);
},
componentWillUnmount: function() {
this.props.activeRoomStore.off("change", null, this);
},
componentDidMount: function() {
// Adding a class to the document body element from here to ease styling it.
document.body.classList.add("is-standalone-room");
@ -429,7 +440,8 @@ loop.standaloneRoomViews = (function(mozL10n) {
*/
_isScreenShareLoading: function() {
return this.state.receivingScreenShare &&
!this.state.screenShareVideoObject;
!this.state.screenShareVideoObject &&
!this.props.screenSharePosterUrl;
},
render: function() {
@ -456,6 +468,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
localPosterUrl={this.props.localPosterUrl}
localSrcVideoObject={this.state.localSrcVideoObject}
localVideoMuted={this.state.videoMuted}
matchMedia={this.state.matchMedia || window.matchMedia.bind(window)}
remotePosterUrl={this.props.remotePosterUrl}
remoteSrcVideoObject={this.state.remoteSrcVideoObject}
renderRemoteVideo={this.shouldRenderRemoteVideo()}

View File

@ -474,7 +474,9 @@ describe("loop.conversationViews", function () {
describe("OngoingConversationView", function() {
function mountTestComponent(extraProps) {
var props = _.extend({
dispatcher: dispatcher
conversationStore: conversationStore,
dispatcher: dispatcher,
matchMedia: window.matchMedia
}, extraProps);
return TestUtils.renderIntoDocument(
React.createElement(loop.conversationViews.OngoingConversationView, props));
@ -489,15 +491,6 @@ describe("loop.conversationViews", function () {
sinon.match.hasOwn("name", "setupStreamElements"));
});
it("should display an avatar for remote video when the stream is not enabled", function() {
view = mountTestComponent({
mediaConnected: true,
remoteVideoEnabled: false
});
TestUtils.findRenderedComponentWithType(view, sharedViews.AvatarView);
});
it("should display the remote video when the stream is enabled", function() {
conversationStore.setStoreState({
remoteSrcVideoObject: { fake: 1 }
@ -511,16 +504,6 @@ describe("loop.conversationViews", function () {
expect(view.getDOMNode().querySelector(".remote video")).not.eql(null);
});
it("should display an avatar for local video when the stream is not enabled", function() {
view = mountTestComponent({
video: {
enabled: false
}
});
TestUtils.findRenderedComponentWithType(view, sharedViews.AvatarView);
});
it("should display the local video when the stream is enabled", function() {
conversationStore.setStoreState({
localSrcVideoObject: { fake: 1 }

View File

@ -95,7 +95,7 @@
describe("Unexpected Warnings Check", function() {
it("should long only the warnings we expect", function() {
chai.expect(caughtWarnings.length).to.eql(27);
chai.expect(caughtWarnings.length).to.eql(28);
});
});

View File

@ -598,21 +598,6 @@ describe("loop.roomViews", function () {
});
describe("Mute", function() {
it("should render local media as audio-only if video is muted",
function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.SESSION_CONNECTED,
videoMuted: true
});
view = mountTestComponent();
expect(view.getDOMNode().querySelector(".local-stream-audio"))
.not.eql(null);
});
});
describe("Edit Context", function() {
it("should show the form when the edit button is clicked", function() {
view = mountTestComponent();

View File

@ -99,7 +99,7 @@ class Test1BrowserCall(MarionetteTestCase):
self.switch_to_chatbox()
# expect a video container on desktop side
media_container = self.wait_for_element_displayed(By.CLASS_NAME, "media")
media_container = self.wait_for_element_displayed(By.CLASS_NAME, "media-layout")
self.assertEqual(media_container.tag_name, "div", "expect a video container")
self.check_video(".local-video")

View File

@ -21,6 +21,7 @@
"gMozLoopAPI": true,
"mockDb": true,
"mockPushHandler": true,
"OpenBrowserWindow": true,
"promiseDeletedOAuthParams": false,
"promiseOAuthGetRegistration": false,
"promiseOAuthParamsSetup": false,

View File

@ -167,3 +167,19 @@ add_task(function* test_screen_share() {
MozLoopService.setScreenShareState("1", false);
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
});
add_task(function* test_private_browsing_window() {
let win = OpenBrowserWindow({ private: true });
yield new Promise(resolve => {
win.addEventListener("load", function listener() {
win.removeEventListener("load", listener);
resolve();
});
});
let button = win.LoopUI.toolbarButton.node;
Assert.ok(button, "Loop button should be present");
Assert.ok(button.getAttribute("disabled"), "Disabled attribute should be set");
win.close();
});

View File

@ -916,26 +916,6 @@ describe("loop.store.ActiveRoomStore", function () {
});
});
it("should retry publishing if on desktop, and in the videoMuted state", function() {
store._isDesktop = true;
store.connectionFailure(new sharedActions.ConnectionFailure({
reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
}));
sinon.assert.calledOnce(fakeSdkDriver.retryPublishWithoutVideo);
});
it("should set videoMuted to try when retrying publishing", function() {
store._isDesktop = true;
store.connectionFailure(new sharedActions.ConnectionFailure({
reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
}));
expect(store.getStoreState().videoMuted).eql(true);
});
it("should store the failure reason", function() {
store.connectionFailure(connectionFailureAction);

View File

@ -147,26 +147,6 @@ describe("loop.store.ConversationStore", function () {
store.setStoreState({windowId: "42"});
});
it("should retry publishing if on desktop, and in the videoMuted state", function() {
store._isDesktop = true;
store.connectionFailure(new sharedActions.ConnectionFailure({
reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
}));
sinon.assert.calledOnce(sdkDriver.retryPublishWithoutVideo);
});
it("should set videoMuted to try when retrying publishing", function() {
store._isDesktop = true;
store.connectionFailure(new sharedActions.ConnectionFailure({
reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
}));
expect(store.getStoreState().videoMuted).eql(true);
});
it("should disconnect the session", function() {
store.connectionFailure(
new sharedActions.ConnectionFailure({reason: "fake"}));

View File

@ -133,43 +133,6 @@ describe("loop.OTSdkDriver", function () {
});
});
describe("#retryPublishWithoutVideo", function() {
beforeEach(function() {
sdk.initPublisher.returns(publisher);
driver.setupStreamElements(new sharedActions.SetupStreamElements({
publisherConfig: publisherConfig
}));
});
it("should make MediaStreamTrack.getSources return without a video source", function(done) {
driver.retryPublishWithoutVideo();
window.MediaStreamTrack.getSources(function(sources) {
expect(sources.some(function(src) {
return src.kind === "video";
})).eql(false);
done();
});
});
it("should call initPublisher", function() {
driver.retryPublishWithoutVideo();
var expectedConfig = _.extend({
channels: {
text: {}
}
}, publisherConfig);
sinon.assert.calledTwice(sdk.initPublisher);
sinon.assert.calledWith(sdk.initPublisher,
sinon.match.instanceOf(HTMLDivElement),
expectedConfig);
});
});
describe("#setMute", function() {
beforeEach(function() {
sdk.initPublisher.returns(publisher);

View File

@ -56,27 +56,6 @@ describe("loop.shared.views.TextChatView", function () {
store.setStoreState({ textChatEnabled: true });
});
it("should add an empty class when the list is empty", function() {
view = mountTestComponent({
messageList: []
});
expect(view.getDOMNode().classList.contains("text-chat-entries-empty")).eql(true);
});
it("should not add an empty class when the list is has items", function() {
view = mountTestComponent({
messageList: [{
type: CHAT_MESSAGE_TYPES.RECEIVED,
contentType: CHAT_CONTENT_TYPES.TEXT,
message: "Hello!",
receivedTimestamp: "2015-06-25T17:53:55.357Z"
}]
});
expect(view.getDOMNode().classList.contains("text-chat-entries-empty")).eql(false);
});
it("should render message entries when message were sent/ received", function() {
view = mountTestComponent({
messageList: [{
@ -297,6 +276,41 @@ describe("loop.shared.views.TextChatView", function () {
fakeServer.restore();
});
it("should add a disabled class when text chat is disabled", function() {
view = mountTestComponent();
store.setStoreState({ textChatEnabled: false });
expect(view.getDOMNode().classList.contains("text-chat-disabled")).eql(true);
});
it("should not a disabled class when text chat is enabled", function() {
view = mountTestComponent();
store.setStoreState({ textChatEnabled: true });
expect(view.getDOMNode().classList.contains("text-chat-disabled")).eql(false);
});
it("should add an empty class when the entries list is empty", function() {
view = mountTestComponent();
expect(view.getDOMNode().classList.contains("text-chat-entries-empty")).eql(true);
});
it("should not add an empty class when the entries list is has items", function() {
view = mountTestComponent();
store.sendTextChatMessage({
contentType: CHAT_CONTENT_TYPES.TEXT,
message: "Hello!",
sentTimestamp: "1970-01-01T00:02:00.000Z",
receivedTimestamp: "1970-01-01T00:02:00.000Z"
});
expect(view.getDOMNode().classList.contains("text-chat-entries-empty")).eql(false);
});
it("should show timestamps from msgs sent more than 1 min apart", function() {
view = mountTestComponent();
@ -326,12 +340,6 @@ describe("loop.shared.views.TextChatView", function () {
.to.eql(2);
});
it("should display the view if no messages and text chat is enabled", function() {
view = mountTestComponent();
expect(view.getDOMNode()).not.eql(null);
});
it("should render message entries when message were sent/ received", function() {
view = mountTestComponent();

View File

@ -1057,6 +1057,7 @@ describe("loop.shared.views", function() {
isRemoteLoading: false,
isScreenShareLoading: false,
localVideoMuted: false,
matchMedia: window.matchMedia,
renderRemoteVideo: false,
showContextRoomName: false,
useDesktopPaths: false
@ -1144,5 +1145,35 @@ describe("loop.shared.views", function() {
expect(view.getDOMNode().querySelector(".media-wrapper")
.classList.contains("showing-local-streams")).eql(true);
});
it("should not mark the wrapper as showing remote streams when not displaying a stream", function() {
view = mountTestComponent({
remoteSrcVideoObject: null,
remotePosterUrl: null
});
expect(view.getDOMNode().querySelector(".media-wrapper")
.classList.contains("showing-remote-streams")).eql(false);
});
it("should mark the wrapper as showing remote streams when displaying a stream", function() {
view = mountTestComponent({
remoteSrcVideoObject: {},
remotePosterUrl: null
});
expect(view.getDOMNode().querySelector(".media-wrapper")
.classList.contains("showing-remote-streams")).eql(true);
});
it("should mark the wrapper as showing remote streams when displaying a poster url", function() {
view = mountTestComponent({
remoteSrcVideoObject: {},
remotePosterUrl: "fake/url"
});
expect(view.getDOMNode().querySelector(".media-wrapper")
.classList.contains("showing-remote-streams")).eql(true);
});
});
});

View File

@ -75,13 +75,25 @@
var dispatcher = new loop.Dispatcher();
var mockSDK = _.extend({
var MockSDK = function() {
dispatcher.register(this, [
"setupStreamElements"
]);
};
MockSDK.prototype = {
setupStreamElements: function() {
// Dummy function to stop warnings.
},
sendTextChatMessage: function(message) {
dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
message: message.message
}));
}
}, Backbone.Events);
};
var mockSDK = new MockSDK();
/**
* Every view that uses an activeRoomStore needs its own; if they shared
@ -116,7 +128,6 @@
});
store.forcedUpdate = function forcedUpdate(contentWindow) {
// Since this is called by setTimeout, we don't want to lose any
// exceptions if there's a problem and we need to debug, so...
try {
@ -136,6 +147,17 @@
camera: {height: 480, orientation: 0, width: 640}
},
remoteVideoEnabled: options.remoteVideoEnabled,
// Override the matchMedia, this is so that the correct version is
// used for the frame.
//
// Currently, we use an icky hack, and the showcase conspires with
// react-frame-component to set iframe.contentWindow.matchMedia onto
// the store. Once React context matures a bit (somewhere between
// 0.14 and 1.0, apparently):
//
// https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based
//
// we should be able to use those to clean this up.
matchMedia: contentWindow.matchMedia.bind(contentWindow),
roomState: options.roomState,
videoMuted: !!options.videoMuted
@ -185,6 +207,10 @@
roomState: ROOM_STATES.HAS_PARTICIPANTS
});
var updatingMobileActiveRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS
});
var localFaceMuteRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
videoMuted: true
@ -201,12 +227,19 @@
receivingScreenShare: true
});
var updatingSharingRoomMobileStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
receivingScreenShare: true
});
var loadingRemoteLoadingScreenStore = makeActiveRoomStore({
mediaConnected: false,
receivingScreenShare: true,
roomState: ROOM_STATES.HAS_PARTICIPANTS,
remoteSrcVideoObject: false
});
var loadingScreenSharingRoomStore = makeActiveRoomStore({
receivingScreenShare: true,
roomState: ROOM_STATES.HAS_PARTICIPANTS
});
@ -234,7 +267,10 @@
});
var invitationRoomStore = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop
mozLoop: navigator.mozLoop,
activeRoomStore: makeActiveRoomStore({
roomState: ROOM_STATES.INIT
})
});
var roomStore = new loop.store.RoomStore(dispatcher, {
@ -253,6 +289,20 @@
})
});
var desktopRoomStoreMedium = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop,
activeRoomStore: makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS
})
});
var desktopRoomStoreLarge = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop,
activeRoomStore: makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS
})
});
var desktopLocalFaceMuteActiveRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
videoMuted: true
@ -272,15 +322,59 @@
activeRoomStore: desktopRemoteFaceMuteActiveRoomStore
});
var conversationStore = new loop.store.ConversationStore(dispatcher, {
client: {},
mozLoop: navigator.mozLoop,
sdkDriver: mockSDK
});
var textChatStore = new loop.store.TextChatStore(dispatcher, {
sdkDriver: mockSDK
});
/**
* Every view that uses an conversationStore needs its own; if they shared
* a conversation store, they'd interfere with each other.
*
* @param options
* @returns {loop.store.ConversationStore}
*/
function makeConversationStore() {
var roomDispatcher = new loop.Dispatcher();
var store = new loop.store.ConversationStore(dispatcher, {
client: {},
mozLoop: navigator.mozLoop,
sdkDriver: mockSDK
});
store.forcedUpdate = function forcedUpdate(contentWindow) {
// Since this is called by setTimeout, we don't want to lose any
// exceptions if there's a problem and we need to debug, so...
try {
var newStoreState = {
// Override the matchMedia, this is so that the correct version is
// used for the frame.
//
// Currently, we use an icky hack, and the showcase conspires with
// react-frame-component to set iframe.contentWindow.matchMedia onto
// the store. Once React context matures a bit (somewhere between
// 0.14 and 1.0, apparently):
//
// https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based
//
// we should be able to use those to clean this up.
matchMedia: contentWindow.matchMedia.bind(contentWindow)
};
store.setStoreState(newStoreState);
} catch (ex) {
console.error("exception in forcedUpdate:", ex);
}
};
return store;
}
var conversationStores = [];
for (var index = 0; index < 5; index++) {
conversationStores[index] = makeConversationStore();
}
// Update the text chat store with the room info.
textChatStore.updateRoomInfo(new sharedActions.UpdateRoomInfo({
roomName: "A Very Long Conversation Name",
@ -341,7 +435,7 @@
loop.store.StoreMixin.register({
activeRoomStore: activeRoomStore,
conversationStore: conversationStore,
conversationStore: conversationStores[0],
textChatStore: textChatStore
});
@ -360,14 +454,6 @@
requestCallUrlInfo: noop
};
var mockConversationModel = new loop.shared.models.ConversationModel({
callerId: "Mrs Jones",
urlCreationDate: (new Date() / 1000).toString()
}, {
sdk: mockSDK
});
mockConversationModel.startSession = noop;
var mockWebSocket = new loop.CallConnectionWebSocket({
url: "fake",
callId: "fakeId",
@ -763,7 +849,7 @@
React.createElement("div", {className: "fx-embedded"},
React.createElement(CallFailedView, {dispatcher: dispatcher,
outgoing: false,
store: conversationStore})
store: conversationStores[0]})
)
),
React.createElement(Example, {dashed: true,
@ -772,7 +858,7 @@
React.createElement("div", {className: "fx-embedded"},
React.createElement(CallFailedView, {dispatcher: dispatcher,
outgoing: true,
store: conversationStore})
store: conversationStores[1]})
)
),
React.createElement(Example, {dashed: true,
@ -781,18 +867,22 @@
React.createElement("div", {className: "fx-embedded"},
React.createElement(CallFailedView, {dispatcher: dispatcher, emailLinkError: true,
outgoing: true,
store: conversationStore})
store: conversationStores[0]})
)
)
),
React.createElement(Section, {name: "OngoingConversationView"},
React.createElement(FramedExample, {height: 254,
summary: "Desktop ongoing conversation window",
width: 298},
React.createElement(FramedExample, {
dashed: true,
height: 394,
onContentsRendered: conversationStores[0].forcedUpdate,
summary: "Desktop ongoing conversation window",
width: 298},
React.createElement("div", {className: "fx-embedded"},
React.createElement(OngoingConversationView, {
audio: {enabled: true},
conversationStore: conversationStores[0],
dispatcher: dispatcher,
localPosterUrl: "sample-img/video-screen-local.png",
mediaConnected: true,
@ -802,28 +892,55 @@
)
),
React.createElement(FramedExample, {height: 600,
summary: "Desktop ongoing conversation window large",
width: 800},
React.createElement("div", {className: "fx-embedded"},
React.createElement(OngoingConversationView, {
audio: {enabled: true},
dispatcher: dispatcher,
localPosterUrl: "sample-img/video-screen-local.png",
mediaConnected: true,
remotePosterUrl: "sample-img/video-screen-remote.png",
remoteVideoEnabled: true,
video: {enabled: true}})
)
React.createElement(FramedExample, {
dashed: true,
height: 400,
onContentsRendered: conversationStores[1].forcedUpdate,
summary: "Desktop ongoing conversation window (medium)",
width: 600},
React.createElement("div", {className: "fx-embedded"},
React.createElement(OngoingConversationView, {
audio: {enabled: true},
conversationStore: conversationStores[1],
dispatcher: dispatcher,
localPosterUrl: "sample-img/video-screen-local.png",
mediaConnected: true,
remotePosterUrl: "sample-img/video-screen-remote.png",
remoteVideoEnabled: true,
video: {enabled: true}})
)
),
React.createElement(FramedExample, {height: 254,
React.createElement(FramedExample, {
height: 600,
onContentsRendered: conversationStores[2].forcedUpdate,
summary: "Desktop ongoing conversation window (large)",
width: 800},
React.createElement("div", {className: "fx-embedded"},
React.createElement(OngoingConversationView, {
audio: {enabled: true},
conversationStore: conversationStores[2],
dispatcher: dispatcher,
localPosterUrl: "sample-img/video-screen-local.png",
mediaConnected: true,
remotePosterUrl: "sample-img/video-screen-remote.png",
remoteVideoEnabled: true,
video: {enabled: true}})
)
),
React.createElement(FramedExample, {
dashed: true,
height: 394,
onContentsRendered: conversationStores[3].forcedUpdate,
summary: "Desktop ongoing conversation window - local face mute",
width: 298},
React.createElement("div", {className: "fx-embedded"},
React.createElement(OngoingConversationView, {
audio: {enabled: true},
conversationStore: conversationStores[3],
dispatcher: dispatcher,
localPosterUrl: "sample-img/video-screen-local.png",
mediaConnected: true,
remotePosterUrl: "sample-img/video-screen-remote.png",
remoteVideoEnabled: true,
@ -831,15 +948,19 @@
)
),
React.createElement(FramedExample, {height: 254,
React.createElement(FramedExample, {
dashed: true, height: 394,
onContentsRendered: conversationStores[4].forcedUpdate,
summary: "Desktop ongoing conversation window - remote face mute",
width: 298},
React.createElement("div", {className: "fx-embedded"},
React.createElement(OngoingConversationView, {
audio: {enabled: true},
conversationStore: conversationStores[4],
dispatcher: dispatcher,
localPosterUrl: "sample-img/video-screen-local.png",
mediaConnected: true,
remotePosterUrl: "sample-img/video-screen-remote.png",
remoteVideoEnabled: false,
video: {enabled: true}})
)
@ -894,7 +1015,8 @@
React.createElement(Section, {name: "DesktopRoomConversationView"},
React.createElement(FramedExample, {
height: 254,
height: 398,
onContentsRendered: invitationRoomStore.activeRoomStore.forcedUpdate,
summary: "Desktop room conversation (invitation, text-chat inclusion/scrollbars don't happen in real client)",
width: 298},
React.createElement("div", {className: "fx-embedded"},
@ -911,6 +1033,7 @@
React.createElement(FramedExample, {
dashed: true,
height: 394,
onContentsRendered: desktopRoomStoreLoading.activeRoomStore.forcedUpdate,
summary: "Desktop room conversation (loading)",
width: 298},
/* Hide scrollbars here. Rotating loading div overflows and causes
@ -927,8 +1050,12 @@
)
),
React.createElement(FramedExample, {height: 254,
summary: "Desktop room conversation"},
React.createElement(FramedExample, {
dashed: true,
height: 394,
onContentsRendered: roomStore.activeRoomStore.forcedUpdate,
summary: "Desktop room conversation",
width: 298},
React.createElement("div", {className: "fx-embedded"},
React.createElement(DesktopRoomConversationView, {
dispatcher: dispatcher,
@ -941,10 +1068,48 @@
)
),
React.createElement(FramedExample, {dashed: true,
height: 394,
summary: "Desktop room conversation local face-mute",
width: 298},
React.createElement(FramedExample, {
dashed: true,
height: 482,
onContentsRendered: desktopRoomStoreMedium.activeRoomStore.forcedUpdate,
summary: "Desktop room conversation (medium)",
width: 602},
React.createElement("div", {className: "fx-embedded"},
React.createElement(DesktopRoomConversationView, {
dispatcher: dispatcher,
localPosterUrl: "sample-img/video-screen-local.png",
mozLoop: navigator.mozLoop,
onCallTerminated: function(){},
remotePosterUrl: "sample-img/video-screen-remote.png",
roomState: ROOM_STATES.HAS_PARTICIPANTS,
roomStore: desktopRoomStoreMedium})
)
),
React.createElement(FramedExample, {
dashed: true,
height: 485,
onContentsRendered: desktopRoomStoreLarge.activeRoomStore.forcedUpdate,
summary: "Desktop room conversation (large)",
width: 646},
React.createElement("div", {className: "fx-embedded"},
React.createElement(DesktopRoomConversationView, {
dispatcher: dispatcher,
localPosterUrl: "sample-img/video-screen-local.png",
mozLoop: navigator.mozLoop,
onCallTerminated: function(){},
remotePosterUrl: "sample-img/video-screen-remote.png",
roomState: ROOM_STATES.HAS_PARTICIPANTS,
roomStore: desktopRoomStoreLarge})
)
),
React.createElement(FramedExample, {
dashed: true,
height: 394,
onContentsRendered: desktopLocalFaceMuteRoomStore.activeRoomStore.forcedUpdate,
summary: "Desktop room conversation local face-mute",
width: 298},
React.createElement("div", {className: "fx-embedded"},
React.createElement(DesktopRoomConversationView, {
dispatcher: dispatcher,
@ -955,7 +1120,9 @@
)
),
React.createElement(FramedExample, {dashed: true, height: 394,
React.createElement(FramedExample, {dashed: true,
height: 394,
onContentsRendered: desktopRemoteFaceMuteRoomStore.activeRoomStore.forcedUpdate,
summary: "Desktop room conversation remote face-mute",
width: 298},
React.createElement("div", {className: "fx-embedded"},
@ -964,6 +1131,7 @@
localPosterUrl: "sample-img/video-screen-local.png",
mozLoop: navigator.mozLoop,
onCallTerminated: function(){},
remotePosterUrl: "sample-img/video-screen-remote.png",
roomStore: desktopRemoteFaceMuteRoomStore})
)
)
@ -1081,8 +1249,7 @@
isFirefox: true,
localPosterUrl: "sample-img/video-screen-local.png",
remotePosterUrl: "sample-img/video-screen-remote.png",
roomState: ROOM_STATES.HAS_PARTICIPANTS,
screenSharePosterUrl: "sample-img/video-screen-baz.png"})
roomState: ROOM_STATES.HAS_PARTICIPANTS})
)
),
@ -1102,8 +1269,7 @@
isFirefox: true,
localPosterUrl: "sample-img/video-screen-local.png",
remotePosterUrl: "sample-img/video-screen-remote.png",
roomState: ROOM_STATES.HAS_PARTICIPANTS,
screenSharePosterUrl: "sample-img/video-screen-baz.png"})
roomState: ROOM_STATES.HAS_PARTICIPANTS})
)
),
@ -1171,12 +1337,12 @@
cssClass: "standalone",
dashed: true,
height: 480,
onContentsRendered: updatingActiveRoomStore.forcedUpdate,
onContentsRendered: updatingMobileActiveRoomStore.forcedUpdate,
summary: "Standalone room conversation (has-participants, 600x480)",
width: 600},
React.createElement("div", {className: "standalone"},
React.createElement(StandaloneRoomView, {
activeRoomStore: updatingActiveRoomStore,
activeRoomStore: updatingMobileActiveRoomStore,
dispatcher: dispatcher,
isFirefox: true,
localPosterUrl: "sample-img/video-screen-local.png",
@ -1189,12 +1355,12 @@
cssClass: "standalone",
dashed: true,
height: 480,
onContentsRendered: updatingSharingRoomStore.forcedUpdate,
onContentsRendered: updatingSharingRoomMobileStore.forcedUpdate,
summary: "Standalone room convo (has-participants, receivingScreenShare, 600x480)",
width: 600},
React.createElement("div", {className: "standalone", cssClass: "standalone"},
React.createElement(StandaloneRoomView, {
activeRoomStore: updatingSharingRoomStore,
activeRoomStore: updatingSharingRoomMobileStore,
dispatcher: dispatcher,
isFirefox: true,
localPosterUrl: "sample-img/video-screen-local.png",
@ -1282,7 +1448,7 @@
// 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 = 23;
var expectedWarningsCount = 18;
var warningsMismatch = caughtWarnings.length !== expectedWarningsCount;
if (uncaughtError || warningsMismatch) {
$("#results").append("<div class='failures'><em>" +

View File

@ -75,13 +75,25 @@
var dispatcher = new loop.Dispatcher();
var mockSDK = _.extend({
var MockSDK = function() {
dispatcher.register(this, [
"setupStreamElements"
]);
};
MockSDK.prototype = {
setupStreamElements: function() {
// Dummy function to stop warnings.
},
sendTextChatMessage: function(message) {
dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
message: message.message
}));
}
}, Backbone.Events);
};
var mockSDK = new MockSDK();
/**
* Every view that uses an activeRoomStore needs its own; if they shared
@ -116,7 +128,6 @@
});
store.forcedUpdate = function forcedUpdate(contentWindow) {
// Since this is called by setTimeout, we don't want to lose any
// exceptions if there's a problem and we need to debug, so...
try {
@ -136,6 +147,17 @@
camera: {height: 480, orientation: 0, width: 640}
},
remoteVideoEnabled: options.remoteVideoEnabled,
// Override the matchMedia, this is so that the correct version is
// used for the frame.
//
// Currently, we use an icky hack, and the showcase conspires with
// react-frame-component to set iframe.contentWindow.matchMedia onto
// the store. Once React context matures a bit (somewhere between
// 0.14 and 1.0, apparently):
//
// https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based
//
// we should be able to use those to clean this up.
matchMedia: contentWindow.matchMedia.bind(contentWindow),
roomState: options.roomState,
videoMuted: !!options.videoMuted
@ -185,6 +207,10 @@
roomState: ROOM_STATES.HAS_PARTICIPANTS
});
var updatingMobileActiveRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS
});
var localFaceMuteRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
videoMuted: true
@ -201,12 +227,19 @@
receivingScreenShare: true
});
var updatingSharingRoomMobileStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
receivingScreenShare: true
});
var loadingRemoteLoadingScreenStore = makeActiveRoomStore({
mediaConnected: false,
receivingScreenShare: true,
roomState: ROOM_STATES.HAS_PARTICIPANTS,
remoteSrcVideoObject: false
});
var loadingScreenSharingRoomStore = makeActiveRoomStore({
receivingScreenShare: true,
roomState: ROOM_STATES.HAS_PARTICIPANTS
});
@ -234,7 +267,10 @@
});
var invitationRoomStore = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop
mozLoop: navigator.mozLoop,
activeRoomStore: makeActiveRoomStore({
roomState: ROOM_STATES.INIT
})
});
var roomStore = new loop.store.RoomStore(dispatcher, {
@ -253,6 +289,20 @@
})
});
var desktopRoomStoreMedium = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop,
activeRoomStore: makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS
})
});
var desktopRoomStoreLarge = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop,
activeRoomStore: makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS
})
});
var desktopLocalFaceMuteActiveRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
videoMuted: true
@ -272,15 +322,59 @@
activeRoomStore: desktopRemoteFaceMuteActiveRoomStore
});
var conversationStore = new loop.store.ConversationStore(dispatcher, {
client: {},
mozLoop: navigator.mozLoop,
sdkDriver: mockSDK
});
var textChatStore = new loop.store.TextChatStore(dispatcher, {
sdkDriver: mockSDK
});
/**
* Every view that uses an conversationStore needs its own; if they shared
* a conversation store, they'd interfere with each other.
*
* @param options
* @returns {loop.store.ConversationStore}
*/
function makeConversationStore() {
var roomDispatcher = new loop.Dispatcher();
var store = new loop.store.ConversationStore(dispatcher, {
client: {},
mozLoop: navigator.mozLoop,
sdkDriver: mockSDK
});
store.forcedUpdate = function forcedUpdate(contentWindow) {
// Since this is called by setTimeout, we don't want to lose any
// exceptions if there's a problem and we need to debug, so...
try {
var newStoreState = {
// Override the matchMedia, this is so that the correct version is
// used for the frame.
//
// Currently, we use an icky hack, and the showcase conspires with
// react-frame-component to set iframe.contentWindow.matchMedia onto
// the store. Once React context matures a bit (somewhere between
// 0.14 and 1.0, apparently):
//
// https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based
//
// we should be able to use those to clean this up.
matchMedia: contentWindow.matchMedia.bind(contentWindow)
};
store.setStoreState(newStoreState);
} catch (ex) {
console.error("exception in forcedUpdate:", ex);
}
};
return store;
}
var conversationStores = [];
for (var index = 0; index < 5; index++) {
conversationStores[index] = makeConversationStore();
}
// Update the text chat store with the room info.
textChatStore.updateRoomInfo(new sharedActions.UpdateRoomInfo({
roomName: "A Very Long Conversation Name",
@ -341,7 +435,7 @@
loop.store.StoreMixin.register({
activeRoomStore: activeRoomStore,
conversationStore: conversationStore,
conversationStore: conversationStores[0],
textChatStore: textChatStore
});
@ -360,14 +454,6 @@
requestCallUrlInfo: noop
};
var mockConversationModel = new loop.shared.models.ConversationModel({
callerId: "Mrs Jones",
urlCreationDate: (new Date() / 1000).toString()
}, {
sdk: mockSDK
});
mockConversationModel.startSession = noop;
var mockWebSocket = new loop.CallConnectionWebSocket({
url: "fake",
callId: "fakeId",
@ -763,7 +849,7 @@
<div className="fx-embedded">
<CallFailedView dispatcher={dispatcher}
outgoing={false}
store={conversationStore} />
store={conversationStores[0]} />
</div>
</Example>
<Example dashed={true}
@ -772,7 +858,7 @@
<div className="fx-embedded">
<CallFailedView dispatcher={dispatcher}
outgoing={true}
store={conversationStore} />
store={conversationStores[1]} />
</div>
</Example>
<Example dashed={true}
@ -781,18 +867,22 @@
<div className="fx-embedded">
<CallFailedView dispatcher={dispatcher} emailLinkError={true}
outgoing={true}
store={conversationStore} />
store={conversationStores[0]} />
</div>
</Example>
</Section>
<Section name="OngoingConversationView">
<FramedExample height={254}
summary="Desktop ongoing conversation window"
width={298}>
<FramedExample
dashed={true}
height={394}
onContentsRendered={conversationStores[0].forcedUpdate}
summary="Desktop ongoing conversation window"
width={298}>
<div className="fx-embedded">
<OngoingConversationView
audio={{enabled: true}}
conversationStore={conversationStores[0]}
dispatcher={dispatcher}
localPosterUrl="sample-img/video-screen-local.png"
mediaConnected={true}
@ -802,28 +892,55 @@
</div>
</FramedExample>
<FramedExample height={600}
summary="Desktop ongoing conversation window large"
width={800}>
<div className="fx-embedded">
<OngoingConversationView
audio={{enabled: true}}
dispatcher={dispatcher}
localPosterUrl="sample-img/video-screen-local.png"
mediaConnected={true}
remotePosterUrl="sample-img/video-screen-remote.png"
remoteVideoEnabled={true}
video={{enabled: true}} />
</div>
<FramedExample
dashed={true}
height={400}
onContentsRendered={conversationStores[1].forcedUpdate}
summary="Desktop ongoing conversation window (medium)"
width={600}>
<div className="fx-embedded">
<OngoingConversationView
audio={{enabled: true}}
conversationStore={conversationStores[1]}
dispatcher={dispatcher}
localPosterUrl="sample-img/video-screen-local.png"
mediaConnected={true}
remotePosterUrl="sample-img/video-screen-remote.png"
remoteVideoEnabled={true}
video={{enabled: true}} />
</div>
</FramedExample>
<FramedExample height={254}
<FramedExample
height={600}
onContentsRendered={conversationStores[2].forcedUpdate}
summary="Desktop ongoing conversation window (large)"
width={800}>
<div className="fx-embedded">
<OngoingConversationView
audio={{enabled: true}}
conversationStore={conversationStores[2]}
dispatcher={dispatcher}
localPosterUrl="sample-img/video-screen-local.png"
mediaConnected={true}
remotePosterUrl="sample-img/video-screen-remote.png"
remoteVideoEnabled={true}
video={{enabled: true}} />
</div>
</FramedExample>
<FramedExample
dashed={true}
height={394}
onContentsRendered={conversationStores[3].forcedUpdate}
summary="Desktop ongoing conversation window - local face mute"
width={298} >
<div className="fx-embedded">
<OngoingConversationView
audio={{enabled: true}}
conversationStore={conversationStores[3]}
dispatcher={dispatcher}
localPosterUrl="sample-img/video-screen-local.png"
mediaConnected={true}
remotePosterUrl="sample-img/video-screen-remote.png"
remoteVideoEnabled={true}
@ -831,15 +948,19 @@
</div>
</FramedExample>
<FramedExample height={254}
<FramedExample
dashed={true} height={394}
onContentsRendered={conversationStores[4].forcedUpdate}
summary="Desktop ongoing conversation window - remote face mute"
width={298} >
<div className="fx-embedded">
<OngoingConversationView
audio={{enabled: true}}
conversationStore={conversationStores[4]}
dispatcher={dispatcher}
localPosterUrl="sample-img/video-screen-local.png"
mediaConnected={true}
remotePosterUrl="sample-img/video-screen-remote.png"
remoteVideoEnabled={false}
video={{enabled: true}} />
</div>
@ -894,7 +1015,8 @@
<Section name="DesktopRoomConversationView">
<FramedExample
height={254}
height={398}
onContentsRendered={invitationRoomStore.activeRoomStore.forcedUpdate}
summary="Desktop room conversation (invitation, text-chat inclusion/scrollbars don't happen in real client)"
width={298}>
<div className="fx-embedded">
@ -911,6 +1033,7 @@
<FramedExample
dashed={true}
height={394}
onContentsRendered={desktopRoomStoreLoading.activeRoomStore.forcedUpdate}
summary="Desktop room conversation (loading)"
width={298}>
{/* Hide scrollbars here. Rotating loading div overflows and causes
@ -927,8 +1050,12 @@
</div>
</FramedExample>
<FramedExample height={254}
summary="Desktop room conversation">
<FramedExample
dashed={true}
height={394}
onContentsRendered={roomStore.activeRoomStore.forcedUpdate}
summary="Desktop room conversation"
width={298}>
<div className="fx-embedded">
<DesktopRoomConversationView
dispatcher={dispatcher}
@ -941,10 +1068,48 @@
</div>
</FramedExample>
<FramedExample dashed={true}
height={394}
summary="Desktop room conversation local face-mute"
width={298}>
<FramedExample
dashed={true}
height={482}
onContentsRendered={desktopRoomStoreMedium.activeRoomStore.forcedUpdate}
summary="Desktop room conversation (medium)"
width={602}>
<div className="fx-embedded">
<DesktopRoomConversationView
dispatcher={dispatcher}
localPosterUrl="sample-img/video-screen-local.png"
mozLoop={navigator.mozLoop}
onCallTerminated={function(){}}
remotePosterUrl="sample-img/video-screen-remote.png"
roomState={ROOM_STATES.HAS_PARTICIPANTS}
roomStore={desktopRoomStoreMedium} />
</div>
</FramedExample>
<FramedExample
dashed={true}
height={485}
onContentsRendered={desktopRoomStoreLarge.activeRoomStore.forcedUpdate}
summary="Desktop room conversation (large)"
width={646}>
<div className="fx-embedded">
<DesktopRoomConversationView
dispatcher={dispatcher}
localPosterUrl="sample-img/video-screen-local.png"
mozLoop={navigator.mozLoop}
onCallTerminated={function(){}}
remotePosterUrl="sample-img/video-screen-remote.png"
roomState={ROOM_STATES.HAS_PARTICIPANTS}
roomStore={desktopRoomStoreLarge} />
</div>
</FramedExample>
<FramedExample
dashed={true}
height={394}
onContentsRendered={desktopLocalFaceMuteRoomStore.activeRoomStore.forcedUpdate}
summary="Desktop room conversation local face-mute"
width={298}>
<div className="fx-embedded">
<DesktopRoomConversationView
dispatcher={dispatcher}
@ -955,7 +1120,9 @@
</div>
</FramedExample>
<FramedExample dashed={true} height={394}
<FramedExample dashed={true}
height={394}
onContentsRendered={desktopRemoteFaceMuteRoomStore.activeRoomStore.forcedUpdate}
summary="Desktop room conversation remote face-mute"
width={298} >
<div className="fx-embedded">
@ -964,6 +1131,7 @@
localPosterUrl="sample-img/video-screen-local.png"
mozLoop={navigator.mozLoop}
onCallTerminated={function(){}}
remotePosterUrl="sample-img/video-screen-remote.png"
roomStore={desktopRemoteFaceMuteRoomStore} />
</div>
</FramedExample>
@ -1081,8 +1249,7 @@
isFirefox={true}
localPosterUrl="sample-img/video-screen-local.png"
remotePosterUrl="sample-img/video-screen-remote.png"
roomState={ROOM_STATES.HAS_PARTICIPANTS}
screenSharePosterUrl="sample-img/video-screen-baz.png" />
roomState={ROOM_STATES.HAS_PARTICIPANTS} />
</div>
</FramedExample>
@ -1102,8 +1269,7 @@
isFirefox={true}
localPosterUrl="sample-img/video-screen-local.png"
remotePosterUrl="sample-img/video-screen-remote.png"
roomState={ROOM_STATES.HAS_PARTICIPANTS}
screenSharePosterUrl="sample-img/video-screen-baz.png" />
roomState={ROOM_STATES.HAS_PARTICIPANTS} />
</div>
</FramedExample>
@ -1171,12 +1337,12 @@
cssClass="standalone"
dashed={true}
height={480}
onContentsRendered={updatingActiveRoomStore.forcedUpdate}
onContentsRendered={updatingMobileActiveRoomStore.forcedUpdate}
summary="Standalone room conversation (has-participants, 600x480)"
width={600}>
<div className="standalone">
<StandaloneRoomView
activeRoomStore={updatingActiveRoomStore}
activeRoomStore={updatingMobileActiveRoomStore}
dispatcher={dispatcher}
isFirefox={true}
localPosterUrl="sample-img/video-screen-local.png"
@ -1189,12 +1355,12 @@
cssClass="standalone"
dashed={true}
height={480}
onContentsRendered={updatingSharingRoomStore.forcedUpdate}
onContentsRendered={updatingSharingRoomMobileStore.forcedUpdate}
summary="Standalone room convo (has-participants, receivingScreenShare, 600x480)"
width={600} >
<div className="standalone" cssClass="standalone">
<StandaloneRoomView
activeRoomStore={updatingSharingRoomStore}
activeRoomStore={updatingSharingRoomMobileStore}
dispatcher={dispatcher}
isFirefox={true}
localPosterUrl="sample-img/video-screen-local.png"
@ -1282,7 +1448,7 @@
// 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 = 23;
var expectedWarningsCount = 18;
var warningsMismatch = caughtWarnings.length !== expectedWarningsCount;
if (uncaughtError || warningsMismatch) {
$("#results").append("<div class='failures'><em>" +

View File

@ -4,7 +4,8 @@
* 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/. */
/* globals overlays, StyleInspectorMenu */
/* globals overlays, StyleInspectorMenu, loader, clipboardHelper,
_Iterator, StopIteration */
"use strict";
@ -49,8 +50,7 @@ const HTML_NS = "http://www.w3.org/1999/xhtml";
*
* @constructor
*/
function UpdateProcess(aWin, aGenerator, aOptions)
{
function UpdateProcess(aWin, aGenerator, aOptions) {
this.win = aWin;
this.iter = _Iterator(aGenerator);
this.onItem = aOptions.onItem || function() {};
@ -66,8 +66,7 @@ UpdateProcess.prototype = {
/**
* Schedule a new batch on the main loop.
*/
schedule: function UP_schedule()
{
schedule: function() {
if (this.canceled) {
return;
}
@ -78,8 +77,7 @@ UpdateProcess.prototype = {
* Cancel the running process. onItem will not be called again,
* and onCancel will be called.
*/
cancel: function UP_cancel()
{
cancel: function() {
if (this._timeout) {
this.win.clearTimeout(this._timeout);
this._timeout = 0;
@ -88,7 +86,7 @@ UpdateProcess.prototype = {
this.onCancel();
},
_timeoutHandler: function UP_timeoutHandler() {
_timeoutHandler: function() {
this._timeout = null;
try {
this._runBatch();
@ -104,10 +102,9 @@ UpdateProcess.prototype = {
}
},
_runBatch: function Y_runBatch()
{
_runBatch: function() {
let time = Date.now();
while(!this.canceled) {
while (!this.canceled) {
// Continue until iter.next() throws...
let next = this.iter.next();
this.onItem(next[1]);
@ -120,9 +117,9 @@ UpdateProcess.prototype = {
};
/**
* CssComputedView is a panel that manages the display of a table sorted by style.
* There should be one instance of CssComputedView per style display (of which there
* will generally only be one).
* CssComputedView is a panel that manages the display of a table
* sorted by style. There should be one instance of CssComputedView
* per style display (of which there will generally only be one).
*
* @param {Inspector} inspector toolbox panel
* @param {Document} document The document that will contain the computed view.
@ -142,8 +139,8 @@ function CssComputedView(inspector, document, pageStyle) {
this._outputParser = new OutputParser();
let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
getService(Ci.nsIXULChromeRegistry);
let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]
.getService(Ci.nsIXULChromeRegistry);
this.getRTLAttr = chromeReg.isLocaleRTL("global") ? "rtl" : "ltr";
// Create bound methods.
@ -211,8 +208,7 @@ function CssComputedView(inspector, document, pageStyle) {
* @param {string} aName The key to lookup.
* @returns A localized version of the given key.
*/
CssComputedView.l10n = function CssComputedView_l10n(aName)
{
CssComputedView.l10n = function(aName) {
try {
return CssComputedView._strings.GetStringFromName(aName);
} catch (ex) {
@ -251,8 +247,7 @@ CssComputedView.prototype = {
this.pageStyle = pageStyle;
},
get includeBrowserStyles()
{
get includeBrowserStyles() {
return this.includeBrowserStylesCheckbox.checked;
},
@ -264,8 +259,9 @@ CssComputedView.prototype = {
},
/**
* Update the view with a new selected element.
* The CssComputedView panel will show the style information for the given element.
* Update the view with a new selected element. The CssComputedView panel
* will show the style information for the given element.
*
* @param {NodeFront} aElement The highlighted node to get styles for.
* @returns a promise that will be resolved when highlighting is complete.
*/
@ -383,8 +379,7 @@ CssComputedView.prototype = {
return {type, value};
},
_createPropertyViews: function()
{
_createPropertyViews: function() {
if (this._createViewsPromise) {
return this._createViewsPromise;
}
@ -396,7 +391,8 @@ CssComputedView.prototype = {
this.numVisibleProperties = 0;
let fragment = this.styleDocument.createDocumentFragment();
this._createViewsProcess = new UpdateProcess(this.styleWindow, CssComputedView.propertyNames, {
this._createViewsProcess = new UpdateProcess(
this.styleWindow, CssComputedView.propertyNames, {
onItem: (aPropertyName) => {
// Per-item callback.
let propView = new PropertyView(this, aPropertyName);
@ -426,8 +422,7 @@ CssComputedView.prototype = {
/**
* Refresh the panel content.
*/
refreshPanel: function CssComputedView_refreshPanel()
{
refreshPanel: function() {
if (!this.viewedElement) {
return promise.resolve();
}
@ -443,12 +438,12 @@ CssComputedView.prototype = {
onlyMatched: !this.includeBrowserStyles,
markMatched: true
})
]).then(([createViews, computed]) => {
]).then(([, computed]) => {
if (viewedElement !== this.viewedElement) {
return;
return promise.resolve();
}
this._matchedProperties = new Set;
this._matchedProperties = new Set();
for (let name in computed) {
if (computed[name].matched) {
this._matchedProperties.add(name);
@ -469,7 +464,8 @@ CssComputedView.prototype = {
this._darkStripe = true;
let deferred = promise.defer();
this._refreshProcess = new UpdateProcess(this.styleWindow, this.propertyViews, {
this._refreshProcess = new UpdateProcess(
this.styleWindow, this.propertyViews, {
onItem: (aPropView) => {
aPropView.refresh();
},
@ -511,8 +507,7 @@ CssComputedView.prototype = {
*
* @param {Event} aEvent the DOM Event object.
*/
_onFilterStyles: function(aEvent)
{
_onFilterStyles: function(aEvent) {
let win = this.styleWindow;
if (this._filterChangedTimeout) {
@ -580,8 +575,7 @@ CssComputedView.prototype = {
*
* @param {Event} aEvent the DOM Event object.
*/
_onIncludeBrowserStyles: function(aEvent)
{
_onIncludeBrowserStyles: function(aEvent) {
this.refreshSourceFilter();
this.refreshPanel();
},
@ -592,16 +586,14 @@ CssComputedView.prototype = {
* document or one of thedocument's stylesheets. If .checked is false we
* display all properties including those that come from UA stylesheets.
*/
refreshSourceFilter: function CssComputedView_setSourceFilter()
{
refreshSourceFilter: function() {
this._matchedProperties = null;
this._sourceFilter = this.includeBrowserStyles ?
CssLogic.FILTER.UA :
CssLogic.FILTER.USER;
},
_onSourcePrefChanged: function CssComputedView__onSourcePrefChanged()
{
_onSourcePrefChanged: function() {
for (let propView of this.propertyViews) {
propView.updateSourceLinks();
}
@ -611,8 +603,7 @@ CssComputedView.prototype = {
/**
* The CSS as displayed by the UI.
*/
createStyleViews: function CssComputedView_createStyleViews()
{
createStyleViews: function() {
if (CssComputedView.propertyNames) {
return;
}
@ -641,8 +632,8 @@ CssComputedView.prototype = {
this._createPropertyViews().then(null, e => {
if (!this._isDestroyed) {
console.warn("The creation of property views was cancelled because the " +
"computed-view was destroyed before it was done creating views");
console.warn("The creation of property views was cancelled because " +
"the computed-view was destroyed before it was done creating views");
} else {
console.error(e);
}
@ -654,18 +645,16 @@ CssComputedView.prototype = {
*
* @return {Set} If a property name is in the set, it has matching selectors.
*/
get matchedProperties()
{
return this._matchedProperties || new Set;
get matchedProperties() {
return this._matchedProperties || new Set();
},
/**
* Focus the window on mousedown.
*
* @param aEvent The event object
* @param event The event object
*/
focusWindow: function(aEvent)
{
focusWindow: function(event) {
let win = this.styleDocument.defaultView;
win.focus();
},
@ -707,8 +696,8 @@ CssComputedView.prototype = {
let win = this.styleDocument.defaultView;
let text = win.getSelection().toString().trim();
// Tidy up block headings by moving CSS property names and their values onto
// the same line and inserting a colon between them.
// Tidy up block headings by moving CSS property names and their
// values onto the same line and inserting a colon between them.
let textArray = text.split(/[\r\n]+/);
let result = "";
@ -737,8 +726,7 @@ CssComputedView.prototype = {
/**
* Destructor for CssComputedView.
*/
destroy: function CssComputedView_destroy()
{
destroy: function() {
this.viewedElement = null;
this._outputParser = null;
@ -819,8 +807,7 @@ PropertyInfo.prototype = {
* @param {string} aName the CSS property name for which this PropertyView
* instance will render the rules.
*/
function PropertyView(aTree, aName)
{
function PropertyView(aTree, aName) {
this.tree = aTree;
this.name = aName;
this.getRTLAttr = aTree.getRTLAttr;
@ -864,32 +851,28 @@ PropertyView.prototype = {
* @return {string} the computed style for the current property of the
* currently highlighted element.
*/
get value()
{
get value() {
return this.propertyInfo.value;
},
/**
* An easy way to access the CssPropertyInfo behind this PropertyView.
*/
get propertyInfo()
{
get propertyInfo() {
return this._propertyInfo;
},
/**
* Does the property have any matched selectors?
*/
get hasMatchedSelectors()
{
get hasMatchedSelectors() {
return this.tree.matchedProperties.has(this.name);
},
/**
* Should this property be visible?
*/
get visible()
{
get visible() {
if (!this.tree.viewedElement) {
return false;
}
@ -913,8 +896,7 @@ PropertyView.prototype = {
* Returns the className that should be assigned to the propertyView.
* @return string
*/
get propertyHeaderClassName()
{
get propertyHeaderClassName() {
if (this.visible) {
let isDark = this.tree._darkStripe = !this.tree._darkStripe;
return isDark ? "property-view row-striped" : "property-view";
@ -927,8 +909,7 @@ PropertyView.prototype = {
* container.
* @return string
*/
get propertyContentClassName()
{
get propertyContentClassName() {
if (this.visible) {
let isDark = this.tree._darkStripe;
return isDark ? "property-content row-striped" : "property-content";
@ -940,8 +921,7 @@ PropertyView.prototype = {
* Build the markup for on computed style
* @return Element
*/
buildMain: function PropertyView_buildMain()
{
buildMain: function() {
let doc = this.tree.styleDocument;
// Build the container element
@ -998,8 +978,7 @@ PropertyView.prototype = {
return this.element;
},
buildSelectorContainer: function PropertyView_buildSelectorContainer()
{
buildSelectorContainer: function() {
let doc = this.tree.styleDocument;
let element = doc.createElementNS(HTML_NS, "div");
element.setAttribute("class", this.propertyContentClassName);
@ -1013,8 +992,7 @@ PropertyView.prototype = {
/**
* Refresh the panel's CSS property value.
*/
refresh: function PropertyView_refresh()
{
refresh: function() {
this.element.className = this.propertyHeaderClassName;
this.element.nextElementSibling.className = this.propertyContentClassName;
@ -1051,8 +1029,7 @@ PropertyView.prototype = {
/**
* Refresh the panel matched rules.
*/
refreshMatchedSelectors: function PropertyView_refreshMatchedSelectors()
{
refreshMatchedSelectors: function() {
let hasMatchedSelectors = this.hasMatchedSelectors;
this.matchedSelectorsContainer.parentNode.hidden = !hasMatchedSelectors;
@ -1063,28 +1040,29 @@ PropertyView.prototype = {
}
if (this.matchedExpanded && hasMatchedSelectors) {
return this.tree.pageStyle.getMatchedSelectors(this.tree.viewedElement, this.name).then(matched => {
if (!this.matchedExpanded) {
return;
}
return this.tree.pageStyle
.getMatchedSelectors(this.tree.viewedElement, this.name)
.then(matched => {
if (!this.matchedExpanded) {
return promise.resolve(undefined);
}
this._matchedSelectorResponse = matched;
this._matchedSelectorResponse = matched;
return this._buildMatchedSelectors().then(() => {
this.matchedExpander.setAttribute("open", "");
this.tree.inspector.emit("computed-view-property-expanded");
});
}).then(null, console.error);
} else {
this.matchedSelectorsContainer.innerHTML = "";
this.matchedExpander.removeAttribute("open");
this.tree.inspector.emit("computed-view-property-collapsed");
return promise.resolve(undefined);
return this._buildMatchedSelectors().then(() => {
this.matchedExpander.setAttribute("open", "");
this.tree.inspector.emit("computed-view-property-expanded");
});
}).then(null, console.error);
}
this.matchedSelectorsContainer.innerHTML = "";
this.matchedExpander.removeAttribute("open");
this.tree.inspector.emit("computed-view-property-collapsed");
return promise.resolve(undefined);
},
get matchedSelectors()
{
get matchedSelectors() {
return this._matchedSelectorResponse;
},
@ -1130,13 +1108,13 @@ PropertyView.prototype = {
* Provide access to the matched SelectorViews that we are currently
* displaying.
*/
get matchedSelectorViews()
{
get matchedSelectorViews() {
if (!this._matchedSelectorViews) {
this._matchedSelectorViews = [];
this._matchedSelectorResponse.forEach(
function matchedSelectorViews_convert(aSelectorInfo) {
this._matchedSelectorViews.push(new SelectorView(this.tree, aSelectorInfo));
function(aSelectorInfo) {
let selectorView = new SelectorView(this.tree, aSelectorInfo);
this._matchedSelectorViews.push(selectorView);
}, this);
}
return this._matchedSelectorViews;
@ -1146,8 +1124,7 @@ PropertyView.prototype = {
* Update all the selector source links to reflect whether we're linking to
* original sources (e.g. Sass files).
*/
updateSourceLinks: function PropertyView_updateSourceLinks()
{
updateSourceLinks: function() {
if (!this._matchedSelectorViews) {
return;
}
@ -1162,8 +1139,7 @@ PropertyView.prototype = {
* @param {Event} aEvent Used to determine the class name of the targets click
* event.
*/
onMatchedToggle: function PropertyView_onMatchedToggle(aEvent)
{
onMatchedToggle: function(aEvent) {
if (aEvent.shiftKey) {
return;
}
@ -1175,8 +1151,7 @@ PropertyView.prototype = {
/**
* The action when a user clicks on the MDN help link for a property.
*/
mdnLinkClick: function PropertyView_mdnLinkClick(aEvent)
{
mdnLinkClick: function(aEvent) {
let inspector = this.tree.inspector;
if (inspector.target.tab) {
@ -1189,7 +1164,7 @@ PropertyView.prototype = {
/**
* Destroy this property view, removing event listeners
*/
destroy: function PropertyView_destroy() {
destroy: function() {
this.element.removeEventListener("dblclick", this.onMatchedToggle, false);
this.element.removeEventListener("keydown", this.onKeyDown, false);
this.element = null;
@ -1210,8 +1185,7 @@ PropertyView.prototype = {
* @param CssComputedView aTree, the owning CssComputedView
* @param aSelectorInfo
*/
function SelectorView(aTree, aSelectorInfo)
{
function SelectorView(aTree, aSelectorInfo) {
this.tree = aTree;
this.selectorInfo = aSelectorInfo;
this._cacheStatusNames();
@ -1245,8 +1219,7 @@ SelectorView.prototype = {
*
* @return {void}
*/
_cacheStatusNames: function SelectorView_cacheStatusNames()
{
_cacheStatusNames: function() {
if (SelectorView.STATUS_NAMES.length) {
return;
}
@ -1256,7 +1229,7 @@ SelectorView.prototype = {
if (i > CssLogic.STATUS.UNMATCHED) {
let value = CssComputedView.l10n("rule.status." + status);
// Replace normal spaces with non-breaking spaces
SelectorView.STATUS_NAMES[i] = value.replace(/ /g, '\u00A0');
SelectorView.STATUS_NAMES[i] = value.replace(/ /g, "\u00A0");
}
}
},
@ -1264,21 +1237,18 @@ SelectorView.prototype = {
/**
* A localized version of cssRule.status
*/
get statusText()
{
get statusText() {
return SelectorView.STATUS_NAMES[this.selectorInfo.status];
},
/**
* Get class name for selector depending on status
*/
get statusClass()
{
get statusClass() {
return SelectorView.CLASS_NAMES[this.selectorInfo.status - 1];
},
get href()
{
get href() {
if (this._href) {
return this._href;
}
@ -1287,19 +1257,15 @@ SelectorView.prototype = {
return this._href;
},
get sourceText()
{
get sourceText() {
return this.selectorInfo.sourceText;
},
get value()
{
get value() {
return this.selectorInfo.value;
},
get outputFragment()
{
get outputFragment() {
// Sadly, because this fragment is added to the template by DOM Templater
// we lose any events that are attached. This means that URLs will open in a
// new window. At some point we should fix this by stopping using the
@ -1320,8 +1286,7 @@ SelectorView.prototype = {
* Update the text of the source link to reflect whether we're showing
* original sources or not.
*/
updateSourceLink: function()
{
updateSourceLink: function() {
return this.updateSource().then((oldSource) => {
if (oldSource != this.source && this.tree.element) {
let selector = '[sourcelocation="' + oldSource + '"]';
@ -1337,8 +1302,7 @@ SelectorView.prototype = {
/**
* Update the 'source' store based on our original sources preference.
*/
updateSource: function()
{
updateSource: function() {
let rule = this.selectorInfo.rule;
this.sheet = rule.parentStyleSheet;
@ -1373,8 +1337,7 @@ SelectorView.prototype = {
/**
* Open the style editor if the RETURN key was pressed.
*/
maybeOpenStyleEditor: function(aEvent)
{
maybeOpenStyleEditor: function(aEvent) {
let keyEvent = Ci.nsIDOMKeyEvent;
if (aEvent.keyCode == keyEvent.DOM_VK_RETURN) {
this.openStyleEditor();
@ -1391,8 +1354,7 @@ SelectorView.prototype = {
*
* @param aEvent The click event
*/
openStyleEditor: function(aEvent)
{
openStyleEditor: function(aEvent) {
let inspector = this.tree.inspector;
let rule = this.selectorInfo.rule;
@ -1401,15 +1363,8 @@ SelectorView.prototype = {
//
// If the stylesheet is a content stylesheet we send it to the style
// editor else we display it in the view source window.
let sheet = rule.parentStyleSheet;
if (!sheet || sheet.isSystem) {
let contentDoc = null;
if (this.tree.viewedElement.isLocal_toBeDeprecated()) {
let rawNode = this.tree.viewedElement.rawNode();
if (rawNode) {
contentDoc = rawNode.ownerDocument;
}
}
let parentStyleSheet = rule.parentStyleSheet;
if (!parentStyleSheet || parentStyleSheet.isSystem) {
let toolbox = gDevTools.getToolbox(inspector.target);
toolbox.viewSource(rule.href, rule.line);
return;
@ -1447,7 +1402,7 @@ function createChild(aParent, aTag, aAttributes={}) {
if (aAttributes.hasOwnProperty(attr)) {
if (attr === "textContent") {
elt.textContent = aAttributes[attr];
} else if(attr === "child") {
} else if (attr === "child") {
elt.appendChild(aAttributes[attr]);
} else {
elt.setAttribute(attr, aAttributes[attr]);

View File

@ -5,7 +5,8 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* globals overlays, Services, EventEmitter, StyleInspectorMenu,
clipboardHelper, _strings, domUtils, AutocompletePopup */
clipboardHelper, _strings, domUtils, AutocompletePopup, loader,
osString */
"use strict";
@ -209,7 +210,7 @@ ElementStyle.prototype = {
filter: this.showUserAgentStyles ? "ua" : undefined,
}).then(entries => {
if (this.destroyed) {
return;
return promise.resolve(undefined);
}
// Make sure the dummy element has been created before continuing...
@ -236,14 +237,12 @@ ElementStyle.prototype = {
// We're done with the previous list of rules.
delete this._refreshRules;
return null;
});
}).then(null, e => {
// populate is often called after a setTimeout,
// the connection may already be closed.
if (this.destroyed) {
return;
return promise.resolve(undefined);
}
return promiseWarn(e);
});
@ -636,7 +635,7 @@ Rule.prototype = {
disabled.delete(this.style);
}
let promise = aModifications.apply().then(() => {
let modificationsPromise = aModifications.apply().then(() => {
let cssProps = {};
for (let cssProp of parseDeclarations(this.style.cssText)) {
cssProps[cssProp.name] = cssProp;
@ -668,8 +667,8 @@ Rule.prototype = {
this.elementStyle._changed();
}).then(null, promiseWarn);
this._applyingModifications = promise;
return promise;
this._applyingModifications = modificationsPromise;
return modificationsPromise;
},
/**
@ -1111,7 +1110,8 @@ TextProperty.prototype = {
*/
stringifyProperty: function() {
// Get the displayed property value
let declaration = this.name + ": " + this.editor.committed.value + ";";
let declaration = this.name + ": " + this.editor.valueSpan.textContent +
";";
// Comment out property declarations that are not enabled
if (!this.enabled) {
@ -1741,7 +1741,7 @@ CssRuleView.prototype = {
refreshPanel: function() {
// Ignore refreshes during editing or when no element is selected.
if (this.isEditing || !this._elementStyle) {
return;
return promise.resolve(undefined);
}
// Repopulate the element style once the current modifications are done.
@ -1893,9 +1893,10 @@ CssRuleView.prototype = {
/**
* Creates an expandable container in the rule view
* @param {String} aLabel The label for the container header
* @param {Boolean} isPseudo Whether or not the container will hold
* pseudo element rules
* @param {String} aLabel
* The label for the container header
* @param {Boolean} isPseudo
* Whether or not the container will hold pseudo element rules
* @return {DOMNode} The container element
*/
createExpandableContainer: function(aLabel, isPseudo = false) {
@ -1915,44 +1916,59 @@ CssRuleView.prototype = {
container.classList.add("ruleview-expandable-container");
this.element.appendChild(container);
let toggleContainerVisibility = (isPseudo, showPseudo) => {
let isOpen = twisty.getAttribute("open");
if (isPseudo) {
this._showPseudoElements = !!showPseudo;
Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements",
this.showPseudoElements);
header.classList.toggle("show-expandable-container",
this.showPseudoElements);
isOpen = !this.showPseudoElements;
} else {
header.classList.toggle("show-expandable-container");
}
if (isOpen) {
twisty.removeAttribute("open");
} else {
twisty.setAttribute("open", "true");
}
};
header.addEventListener("dblclick", () => {
toggleContainerVisibility(isPseudo, !this.showPseudoElements);
this._toggleContainerVisibility(twisty, header, isPseudo,
!this.showPseudoElements);
}, false);
twisty.addEventListener("click", () => {
toggleContainerVisibility(isPseudo, !this.showPseudoElements);
this._toggleContainerVisibility(twisty, header, isPseudo,
!this.showPseudoElements);
}, false);
if (isPseudo) {
toggleContainerVisibility(isPseudo, this.showPseudoElements);
this._toggleContainerVisibility(twisty, header, isPseudo,
this.showPseudoElements);
}
return container;
},
/**
* Toggle the visibility of an expandable container
* @param {DOMNode} twisty
* clickable toggle DOM Node
* @param {DOMNode} header
* expandable container header DOM Node
* @param {Boolean} isPseudo
* whether or not the container will hold pseudo element rules
* @param {Boolean} showPseudo
* whether or not pseudo element rules should be displayed
*/
_toggleContainerVisibility: function(twisty, header, isPseudo, showPseudo) {
let isOpen = twisty.getAttribute("open");
if (isPseudo) {
this._showPseudoElements = !!showPseudo;
Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements",
this.showPseudoElements);
header.classList.toggle("show-expandable-container",
this.showPseudoElements);
isOpen = !this.showPseudoElements;
} else {
header.classList.toggle("show-expandable-container");
}
if (isOpen) {
twisty.removeAttribute("open");
} else {
twisty.setAttribute("open", "true");
}
},
_getRuleViewHeaderClassName: function(isPseudo) {
let baseClassName = "theme-gutter ruleview-header";
return isPseudo ? baseClassName + " ruleview-expandable-header" :

View File

@ -231,6 +231,7 @@ StyleInspectorMenu.prototype = {
this.menuitemCopy.hidden = !this._hasTextSelected();
this.menuitemCopyColor.hidden = !this._isColorPopup();
this.menuitemCopyImageDataUrl.hidden = !this._isImageUrl();
this.menuitemCopyUrl.hidden = !this._isImageUrl();
this.menuitemCopyRule.hidden = true;
this.menuitemCopyLocation.hidden = true;
@ -378,6 +379,10 @@ StyleInspectorMenu.prototype = {
* Retrieve the url for the selected image and copy it to the clipboard
*/
_onCopyUrl: function() {
if (!this._clickedNodeInfo) {
return;
}
clipboardHelper.copyString(this._clickedNodeInfo.value.url);
},

View File

@ -52,6 +52,20 @@ add_task(function*() {
copyRule: false
}
},
{
desc: "Test Copy Property Value with Priority",
node: ruleEditor.rule.textProps[3].editor.valueSpan,
menuItem: contextmenu.menuitemCopyPropertyValue,
expectedPattern: "#00F !important",
hidden: {
copyLocation: true,
copyPropertyDeclaration: false,
copyPropertyName: true,
copyPropertyValue: false,
copySelector: true,
copyRule: false
}
},
{
desc: "Test Copy Property Declaration",
node: ruleEditor.rule.textProps[2].editor.nameSpan,
@ -66,6 +80,20 @@ add_task(function*() {
copyRule: false
}
},
{
desc: "Test Copy Property Declaration with Priority",
node: ruleEditor.rule.textProps[3].editor.nameSpan,
menuItem: contextmenu.menuitemCopyPropertyDeclaration,
expectedPattern: "border-color: #00F !important;",
hidden: {
copyLocation: true,
copyPropertyDeclaration: false,
copyPropertyName: false,
copyPropertyValue: true,
copySelector: true,
copyRule: false
}
},
{
desc: "Test Copy Rule",
node: ruleEditor.rule.textProps[2].editor.nameSpan,
@ -74,6 +102,7 @@ add_task(function*() {
"\tcolor: #F00;[\\r\\n]+" +
"\tbackground-color: #00F;[\\r\\n]+" +
"\tfont-size: 12px;[\\r\\n]+" +
"\tborder-color: #00F !important;[\\r\\n]+" +
"}",
hidden: {
copyLocation: true,
@ -124,6 +153,7 @@ add_task(function*() {
"\t\/\\* color: #F00; \\*\/[\\r\\n]+" +
"\tbackground-color: #00F;[\\r\\n]+" +
"\tfont-size: 12px;[\\r\\n]+" +
"\tborder-color: #00F !important;[\\r\\n]+" +
"}",
hidden: {
copyLocation: true,

View File

@ -80,6 +80,7 @@ function* testCopyUrlToClipboard({view, inspector}, type, selector, expected) {
yield popup;
info("Context menu is displayed");
ok(!view._contextmenu.menuitemCopyUrl.hidden, "\"Copy URL\" menu entry is displayed");
ok(!view._contextmenu.menuitemCopyImageDataUrl.hidden, "\"Copy Image Data-URL\" menu entry is displayed");
if (type == "data-uri") {

View File

@ -6,4 +6,5 @@ html, body, #testid {
color: #F00;
background-color: #00F;
font-size: 12px;
border-color: #00F !important;
}

View File

@ -638,6 +638,7 @@
@RESPATH@/components/nsUrlClassifierHashCompleter.js
@RESPATH@/components/nsUrlClassifierListManager.js
@RESPATH@/components/nsUrlClassifierLib.js
@RESPATH@/components/PrivateBrowsingTrackingProtectionWhitelist.js
@RESPATH@/components/url-classifier.xpt
#endif

View File

@ -97,6 +97,9 @@ quit-button.tooltiptext.mac = Quit %1$S (%2$S)
# approval before you change it.
loop-call-button3.label = Hello
loop-call-button3.tooltiptext = Start a conversation
# LOCALIZATION NOTE(loop-call-button3-pb.tooltiptext): Shown when the button is
# placed inside a Private Browsing window. %S is the value of loop-call-button3.label.
loop-call-button3-pb.tooltiptext = %S is not available in Private Browsing
social-share-button.label = Share This Page
social-share-button.tooltiptext = Share this page

View File

@ -206,6 +206,8 @@ active_screenshare_button_title=Stop sharing
inactive_screenshare_button_title=Share your screen
share_tabs_button_title2=Share your Tabs
share_windows_button_title=Share other Windows
self_view_hidden_message=Self-view hidden but still being sent; resize window to show
## LOCALIZATION NOTE (call_with_contact_title): The title displayed
## when calling a contact. Don't translate the part between {{..}} because

View File

@ -115,7 +115,7 @@
.titlebar-button {
border: none;
margin: 0 !important;
padding: 12px 17px;
padding: 10px 17px;
}
#main-window[sizemode=maximized] .titlebar-button {
@ -147,6 +147,20 @@
list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-white);
}
#titlebar-min:-moz-lwtheme {
list-style-image: url(chrome://browser/skin/caption-buttons.svg#minimize-themes);
}
#titlebar-max:-moz-lwtheme {
list-style-image: url(chrome://browser/skin/caption-buttons.svg#maximize-themes);
}
#main-window[sizemode="maximized"] #titlebar-max:-moz-lwtheme {
list-style-image: url(chrome://browser/skin/caption-buttons.svg#restore-themes);
}
#titlebar-close:-moz-lwtheme {
list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-themes);
}
/* the 12px image renders a 10px icon, and the 10px upscaled gets rounded to 12.5, which
* rounds up to 13px, which makes the icon one pixel too big on 1.25dppx. Fix: */
@media (min-resolution: 1.20dppx) and (max-resolution: 1.45dppx) {
@ -222,20 +236,32 @@
background-color: Highlight;
}
#titlebar-min {
list-style-image: url(chrome://browser/skin/caption-buttons.svg#minimize-highcontrast);
}
#titlebar-min:hover {
list-style-image: url(chrome://browser/skin/caption-buttons.svg#minimize-highlight);
list-style-image: url(chrome://browser/skin/caption-buttons.svg#minimize-highcontrast-hover);
}
#titlebar-max {
list-style-image: url(chrome://browser/skin/caption-buttons.svg#maximize-highcontrast);
}
#titlebar-max:hover {
list-style-image: url(chrome://browser/skin/caption-buttons.svg#maximize-highlight);
list-style-image: url(chrome://browser/skin/caption-buttons.svg#maximize-highcontrast-hover);
}
#main-window[sizemode="maximized"] #titlebar-max {
list-style-image: url(chrome://browser/skin/caption-buttons.svg#restore-highcontrast);
}
#main-window[sizemode="maximized"] #titlebar-max:hover {
list-style-image: url(chrome://browser/skin/caption-buttons.svg#restore-highlight);
list-style-image: url(chrome://browser/skin/caption-buttons.svg#restore-highcontrast-hover);
}
#titlebar-close {
list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-highcontrast);
}
#titlebar-close:hover {
list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-highlight);
list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-highcontrast-hover);
}
}
}

View File

@ -659,7 +659,7 @@ toolbar[brighttext] .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker {
-moz-padding-end: 5px;
}
#nav-bar .toolbarbutton-1[type=panel]:not(#back-button):not(#forward-button):not(#feed-button):not(#PanelUI-menu-button),
#nav-bar .toolbarbutton-1[type=panel],
#nav-bar .toolbarbutton-1[type=menu]:not(#back-button):not(#forward-button):not(#feed-button):not(#PanelUI-menu-button) {
padding-left: 5px;
padding-right: 5px;
@ -771,8 +771,8 @@ toolbarbutton[constrain-size="true"][cui-areatype="toolbar"] > .toolbarbutton-ba
width: 32px;
}
#nav-bar .toolbarbutton-1[type=panel]:not(#back-button):not(#forward-button):not(#feed-button):not(#PanelUI-menu-button) > .toolbarbutton-icon,
#nav-bar .toolbarbutton-1[type=panel]:not(#back-button):not(#forward-button):not(#feed-button):not(#PanelUI-menu-button) > .toolbarbutton-badge-container,
#nav-bar .toolbarbutton-1[type=panel] > .toolbarbutton-icon,
#nav-bar .toolbarbutton-1[type=panel] > .toolbarbutton-badge-container,
#nav-bar .toolbarbutton-1[type=menu]:not(#back-button):not(#forward-button):not(#feed-button):not(#PanelUI-menu-button) > .toolbarbutton-icon,
#nav-bar .toolbarbutton-1[type=menu]:not(#back-button):not(#forward-button):not(#feed-button):not(#PanelUI-menu-button) > .toolbarbutton-badge-container,
#nav-bar .toolbarbutton-1[type=menu] > .toolbarbutton-text /* hack for add-ons that forcefully display the label */ {
@ -1203,6 +1203,11 @@ toolbarbutton[constrain-size="true"][cui-areatype="toolbar"] > .toolbarbutton-ba
-moz-padding-end: 2px;
}
/* overlap the urlbar's border */
#PopupAutoCompleteRichResult {
margin-top: -1px;
}
@media (-moz-os-version: windows-xp),
(-moz-os-version: windows-vista),
(-moz-os-version: windows-win7) {
@ -1246,6 +1251,11 @@ toolbarbutton[constrain-size="true"][cui-areatype="toolbar"] > .toolbarbutton-ba
.searchbar-textbox:not(:-moz-lwtheme)[focused] {
box-shadow: 0 0 0 1px Highlight inset;
}
/* overlap the urlbar's border and inset box-shadow */
#PopupAutoCompleteRichResult:not(:-moz-lwtheme) {
margin-top: -2px;
}
}
@media not all and (-moz-os-version: windows-xp) {

View File

@ -9,7 +9,7 @@
fill: none;
}
g:not(#close) {
g:not([id|="close"]) {
shape-rendering: crispEdges;
}
@ -21,17 +21,36 @@
display: initial;
}
[id$="-highlight"] > g {
g.highlight {
stroke-width: 1.9px;
}
g.themes {
stroke: #fff;
stroke-width: 1.9px;
}
.outer-stroke {
stroke: #000;
stroke-width: 3.6;
opacity: .75;
}
.restore-background-window {
stroke-width: .9;
}
[id$="-highcontrast-hover"] > g {
stroke: HighlightText;
}
[id$="-white"] > g {
stroke: #fff;
}
</style>
<g id="close">
<line x1="1" y1="1" x2="11" y2="11"/>
<line x1="11" y1="1" x2="1" y2="11"/>
<path d="M1,1 l 10,10 M1,11 l 10,-10"/>
</g>
<g id="maximize">
<rect x="1.5" y="1.5" width="9" height="9"/>
@ -43,13 +62,46 @@
<rect x="1.5" y="3.5" width="7" height="7"/>
<polyline points="3.5,3.5 3.5,1.5 10.5,1.5 10.5,8.5 8.5,8.5"/>
</g>
<use id="close-highlight" xlink:href="#close"/>
<use id="maximize-highlight" xlink:href="#maximize"/>
<use id="minimize-highlight" xlink:href="#minimize"/>
<use id="restore-highlight" xlink:href="#restore"/>
<use id="close-white" xlink:href="#close"/>
<use id="maximize-white" xlink:href="#maximize"/>
<use id="minimize-white" xlink:href="#minimize"/>
<use id="restore-white" xlink:href="#restore"/>
<g id="close-highcontrast" class="highlight">
<path d="M1,1 l 10,10 M1,11 l 10,-10"/>
</g>
<g id="maximize-highcontrast" class="highlight">
<rect x="2" y="2" width="8" height="8"/>
</g>
<g id="minimize-highcontrast" class="highlight">
<line x1="1" y1="6" x2="11" y2="6"/>
</g>
<g id="restore-highcontrast" class="highlight">
<rect x="2" y="4" width="6" height="6"/>
<polyline points="3.5,1.5 10.5,1.5 10.5,8.5" class="restore-background-window"/>
</g>
<use id="close-highcontrast-hover" xlink:href="#close-highcontrast"/>
<use id="maximize-highcontrast-hover" xlink:href="#maximize-highcontrast"/>
<use id="minimize-highcontrast-hover" xlink:href="#minimize-highcontrast"/>
<use id="restore-highcontrast-hover" xlink:href="#restore-highcontrast"/>
<g id="close-themes" class="themes">
<path d="M1,1 l 10,10 M1,11 l 10,-10" class="outer-stroke" />
<path d="M1.75,1.75 l 8.5,8.5 M1.75,10.25 l 8.5,-8.5"/>
</g>
<g id="maximize-themes" class="themes">
<rect x="2" y="2" width="8" height="8" class="outer-stroke"/>
<rect x="2" y="2" width="8" height="8"/>
</g>
<g id="minimize-themes" class="themes">
<line x1="0" y1="6" x2="12" y2="6" class="outer-stroke"/>
<line x1="1" y1="6" x2="11" y2="6"/>
</g>
<g id="restore-themes" class="themes">
<path d="M2,4 l 6,0 l 0,6 l -6,0z M2.5,1.5 l 8,0 l 0,8" class="outer-stroke"/>
<rect x="2" y="4" width="6" height="6"/>
<polyline points="3.5,1.5 10.5,1.5 10.5,8.5" class="restore-background-window"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -2955,129 +2955,16 @@ nsDocShell::GetRecordProfileTimelineMarkers(bool* aValue)
nsresult
nsDocShell::PopProfileTimelineMarkers(
JSContext* aCx,
JS::MutableHandle<JS::Value> aProfileTimelineMarkers)
JS::MutableHandle<JS::Value> aOut)
{
// Looping over all markers gathered so far at the docShell level, whenever a
// START marker is found, look for the corresponding END marker and build a
// {name,start,end} JS object.
// Paint markers are different because paint is handled at root docShell level
// in the information that a paint was done is then stored at each sub
// docShell level but we can only be sure that a paint did happen in a
// docShell if an Layer marker type was recorded too.
nsTArray<dom::ProfileTimelineMarker> store;
SequenceRooter<dom::ProfileTimelineMarker> rooter(aCx, &store);
nsTArray<mozilla::dom::ProfileTimelineMarker> profileTimelineMarkers;
SequenceRooter<mozilla::dom::ProfileTimelineMarker> rooter(
aCx, &profileTimelineMarkers);
if (!IsObserved()) {
if (!ToJSValue(aCx, profileTimelineMarkers, aProfileTimelineMarkers)) {
JS_ClearPendingException(aCx);
return NS_ERROR_UNEXPECTED;
}
return NS_OK;
if (IsObserved()) {
mObserved->PopMarkers(aCx, store);
}
nsTArray<UniquePtr<TimelineMarker>>& markersStore = mObserved.get()->mTimelineMarkers;
// If we see an unpaired START, we keep it around for the next call
// to PopProfileTimelineMarkers. We store the kept START objects in
// this array.
nsTArray<UniquePtr<TimelineMarker>> keptMarkers;
for (uint32_t i = 0; i < markersStore.Length(); ++i) {
UniquePtr<TimelineMarker>& startPayload = markersStore[i];
const char* startMarkerName = startPayload->GetName();
bool hasSeenPaintedLayer = false;
bool isPaint = strcmp(startMarkerName, "Paint") == 0;
// If we are processing a Paint marker, we append information from
// all the embedded Layer markers to this array.
dom::Sequence<dom::ProfileTimelineLayerRect> layerRectangles;
// If this is a TRACING_TIMESTAMP marker, there's no corresponding "end"
// marker, as it's a single unit of time, not a duration, create the final
// marker here.
if (startPayload->GetMetaData() == TRACING_TIMESTAMP) {
mozilla::dom::ProfileTimelineMarker* marker =
profileTimelineMarkers.AppendElement();
marker->mName = NS_ConvertUTF8toUTF16(startPayload->GetName());
marker->mStart = startPayload->GetTime();
marker->mEnd = startPayload->GetTime();
marker->mStack = startPayload->GetStack();
startPayload->AddDetails(aCx, *marker);
continue;
}
if (startPayload->GetMetaData() == TRACING_INTERVAL_START) {
bool hasSeenEnd = false;
// DOM events can be nested, so we must take care when searching
// for the matching end. It doesn't hurt to apply this logic to
// all event types.
uint32_t markerDepth = 0;
// The assumption is that the devtools timeline flushes markers frequently
// enough for the amount of markers to always be small enough that the
// nested for loop isn't going to be a performance problem.
for (uint32_t j = i + 1; j < markersStore.Length(); ++j) {
UniquePtr<TimelineMarker>& endPayload = markersStore[j];
const char* endMarkerName = endPayload->GetName();
// Look for Layer markers to stream out paint markers.
if (isPaint && strcmp(endMarkerName, "Layer") == 0) {
hasSeenPaintedLayer = true;
endPayload->AddLayerRectangles(layerRectangles);
}
if (!startPayload->Equals(*endPayload)) {
continue;
}
// Pair start and end markers.
if (endPayload->GetMetaData() == TRACING_INTERVAL_START) {
++markerDepth;
} else if (endPayload->GetMetaData() == TRACING_INTERVAL_END) {
if (markerDepth > 0) {
--markerDepth;
} else {
// But ignore paint start/end if no layer has been painted.
if (!isPaint || (isPaint && hasSeenPaintedLayer)) {
mozilla::dom::ProfileTimelineMarker* marker =
profileTimelineMarkers.AppendElement();
marker->mName = NS_ConvertUTF8toUTF16(startPayload->GetName());
marker->mStart = startPayload->GetTime();
marker->mEnd = endPayload->GetTime();
marker->mStack = startPayload->GetStack();
if (isPaint) {
marker->mRectangles.Construct(layerRectangles);
}
startPayload->AddDetails(aCx, *marker);
endPayload->AddDetails(aCx, *marker);
}
// We want the start to be dropped either way.
hasSeenEnd = true;
break;
}
}
}
// If we did not see the corresponding END, keep the START.
if (!hasSeenEnd) {
keptMarkers.AppendElement(Move(markersStore[i]));
markersStore.RemoveElementAt(i);
--i;
}
}
}
markersStore.SwapElements(keptMarkers);
if (!ToJSValue(aCx, profileTimelineMarkers, aProfileTimelineMarkers)) {
if (!ToJSValue(aCx, store, aOut)) {
JS_ClearPendingException(aCx);
return NS_ERROR_UNEXPECTED;
}

View File

@ -34,4 +34,101 @@ ObservedDocShell::ClearMarkers()
mTimelineMarkers.Clear();
}
void
ObservedDocShell::PopMarkers(JSContext* aCx,
nsTArray<dom::ProfileTimelineMarker>& aStore)
{
// If we see an unpaired START, we keep it around for the next call
// to ObservedDocShell::PopMarkers. We store the kept START objects here.
nsTArray<UniquePtr<TimelineMarker>> keptStartMarkers;
for (uint32_t i = 0; i < mTimelineMarkers.Length(); ++i) {
UniquePtr<TimelineMarker>& startPayload = mTimelineMarkers[i];
// If this is a TRACING_TIMESTAMP marker, there's no corresponding END
// as it's a single unit of time, not a duration.
if (startPayload->GetMetaData() == TRACING_TIMESTAMP) {
dom::ProfileTimelineMarker* marker = aStore.AppendElement();
marker->mName = NS_ConvertUTF8toUTF16(startPayload->GetName());
marker->mStart = startPayload->GetTime();
marker->mEnd = startPayload->GetTime();
marker->mStack = startPayload->GetStack();
startPayload->AddDetails(aCx, *marker);
continue;
}
// Whenever a START marker is found, look for the corresponding END
// and build a {name,start,end} JS object.
if (startPayload->GetMetaData() == TRACING_INTERVAL_START) {
bool hasSeenEnd = false;
// "Paint" markers are different because painting is handled at root
// docshell level. The information that a paint was done is stored at
// sub-docshell level, but we can only be sure that a paint did actually
// happen in if a "Layer" marker was recorded too.
bool startIsPaintType = strcmp(startPayload->GetName(), "Paint") == 0;
bool hasSeenLayerType = false;
// If we are processing a "Paint" marker, we append information from
// all the embedded "Layer" markers to this array.
dom::Sequence<dom::ProfileTimelineLayerRect> layerRectangles;
// DOM events can be nested, so we must take care when searching
// for the matching end. It doesn't hurt to apply this logic to
// all event types.
uint32_t markerDepth = 0;
// The assumption is that the devtools timeline flushes markers frequently
// enough for the amount of markers to always be small enough that the
// nested for loop isn't going to be a performance problem.
for (uint32_t j = i + 1; j < mTimelineMarkers.Length(); ++j) {
UniquePtr<TimelineMarker>& endPayload = mTimelineMarkers[j];
bool endIsLayerType = strcmp(endPayload->GetName(), "Layer") == 0;
// Look for "Layer" markers to stream out "Paint" markers.
if (startIsPaintType && endIsLayerType) {
hasSeenLayerType = true;
endPayload->AddLayerRectangles(layerRectangles);
}
if (!startPayload->Equals(*endPayload)) {
continue;
}
if (endPayload->GetMetaData() == TRACING_INTERVAL_START) {
++markerDepth;
continue;
}
if (endPayload->GetMetaData() == TRACING_INTERVAL_END) {
if (markerDepth > 0) {
--markerDepth;
continue;
}
if (!startIsPaintType || (startIsPaintType && hasSeenLayerType)) {
dom::ProfileTimelineMarker* marker = aStore.AppendElement();
marker->mName = NS_ConvertUTF8toUTF16(startPayload->GetName());
marker->mStart = startPayload->GetTime();
marker->mEnd = endPayload->GetTime();
marker->mStack = startPayload->GetStack();
if (hasSeenLayerType) {
marker->mRectangles.Construct(layerRectangles);
}
startPayload->AddDetails(aCx, *marker);
endPayload->AddDetails(aCx, *marker);
}
hasSeenEnd = true;
break;
}
}
// If we did not see the corresponding END, keep the START.
if (!hasSeenEnd) {
keptStartMarkers.AppendElement(Move(mTimelineMarkers[i]));
mTimelineMarkers.RemoveElementAt(i);
--i;
}
}
}
mTimelineMarkers.SwapElements(keptStartMarkers);
}
} // namespace mozilla

View File

@ -15,6 +15,9 @@ class nsDocShell;
class TimelineMarker;
namespace mozilla {
namespace dom {
struct ProfileTimelineMarker;
}
// # ObservedDocShell
//
@ -24,18 +27,16 @@ class ObservedDocShell : public LinkedListElement<ObservedDocShell>
{
private:
nsRefPtr<nsDocShell> mDocShell;
public:
// FIXME: make this private once all marker-specific logic has been
// moved out of nsDocShell.
nsTArray<UniquePtr<TimelineMarker>> mTimelineMarkers;
public:
explicit ObservedDocShell(nsDocShell* aDocShell);
nsDocShell* operator*() const { return mDocShell.get(); }
void AddMarker(const char* aName, TracingMetadata aMetaData);
void AddMarker(UniquePtr<TimelineMarker>&& aMarker);
void ClearMarkers();
void PopMarkers(JSContext* aCx, nsTArray<dom::ProfileTimelineMarker>& aStore);
};
} // namespace mozilla

View File

@ -898,7 +898,7 @@ MediaPipelineFactory::EnsureExternalCodec(VideoSessionConduit& aConduit,
nsCOMPtr<nsIGfxInfo> gfxInfo = do_GetService("@mozilla.org/gfx/info;1");
if (gfxInfo) {
int32_t status;
if (NS_SUCCEEDED(gfxInfo->GetFeatureStatus(nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION, &status))) {
if (NS_SUCCEEDED(gfxInfo->GetFeatureStatus(nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION_ENCODE, &status))) {
if (status != nsIGfxInfo::FEATURE_STATUS_OK) {
NS_WARNING("VP8 encoder hardware is not whitelisted: disabling.\n");
} else {
@ -923,11 +923,10 @@ MediaPipelineFactory::EnsureExternalCodec(VideoSessionConduit& aConduit,
nsCOMPtr<nsIGfxInfo> gfxInfo = do_GetService("@mozilla.org/gfx/info;1");
if (gfxInfo) {
int32_t status;
if (NS_SUCCEEDED(gfxInfo->GetFeatureStatus(nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION, &status))) {
if (NS_SUCCEEDED(gfxInfo->GetFeatureStatus(nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION_DECODE, &status))) {
if (status != nsIGfxInfo::FEATURE_STATUS_OK) {
NS_WARNING("VP8 decoder hardware is not whitelisted: disabling.\n");
} else {
VideoDecoder* decoder;
decoder = MediaCodecVideoCodec::CreateDecoder(MediaCodecVideoCodec::CodecType::CODEC_VP8);
if (decoder) {

View File

@ -64,6 +64,8 @@ public class AppConstants {
* If our MAX_SDK_VERSION is lower than ICS, we must not be an ICS device.
* Otherwise, we need a range check.
*/
public static final boolean preM = MAX_SDK_VERSION < 23 ||
(MIN_SDK_VERSION < 23 && Build.VERSION.SDK_INT < 23 && !Build.VERSION.RELEASE.equals("M"));
public static final boolean preLollipop = MAX_SDK_VERSION < 21 || (MIN_SDK_VERSION < 21 && Build.VERSION.SDK_INT < 21);
public static final boolean preJBMR2 = MAX_SDK_VERSION < 18 || (MIN_SDK_VERSION < 18 && Build.VERSION.SDK_INT < 18);
public static final boolean preJBMR1 = MAX_SDK_VERSION < 17 || (MIN_SDK_VERSION < 17 && Build.VERSION.SDK_INT < 17);

View File

@ -47,6 +47,7 @@ import org.mozilla.gecko.overlays.ui.ShareDialog;
import org.mozilla.gecko.prompts.PromptService;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.GeckoRequest;
import org.mozilla.gecko.util.HardwareCodecCapabilityUtils;
import org.mozilla.gecko.util.HardwareUtils;
import org.mozilla.gecko.util.NativeEventListener;
import org.mozilla.gecko.util.NativeJSContainer;
@ -92,6 +93,7 @@ import android.location.LocationManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
@ -961,6 +963,16 @@ public class GeckoAppShell
return getHandlersForIntent(intent);
}
@WrapElementForJNI(stubName = "GetHWEncoderCapability")
static boolean getHWEncoderCapability() {
return HardwareCodecCapabilityUtils.getHWEncoderCapability();
}
@WrapElementForJNI(stubName = "GetHWDecoderCapability")
static boolean getHWDecoderCapability() {
return HardwareCodecCapabilityUtils.getHWDecoderCapability();
}
static List<ResolveInfo> queryIntentActivities(Intent intent) {
final PackageManager pm = getContext().getPackageManager();

View File

@ -237,6 +237,8 @@ public class BrowserSearch extends HomeFragment
// Fetch engines if we need to.
if (mSearchEngines.isEmpty() || !Locale.getDefault().equals(mLastLocale)) {
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:GetVisible", null));
} else {
updateSearchEngineBar();
}
Telemetry.startUISession(TelemetryContract.Session.FRECENCY);
@ -342,10 +344,6 @@ public class BrowserSearch extends HomeFragment
EventDispatcher.getInstance().registerGeckoThreadListener(this,
"SearchEngines:Data");
// If the view backed by this Fragment is being recreated, we will not receive
// a new search engine data event so refresh the new search engine bar's data
// & Views with the data we have.
updateSearchEngineBar();
mSearchEngineBar.setOnSearchBarClickListener(this);
}

View File

@ -83,6 +83,7 @@ gujar.sources += [
'util/GeckoEventListener.java',
'util/GeckoJarReader.java',
'util/GeckoRequest.java',
'util/HardwareCodecCapabilityUtils.java',
'util/HardwareUtils.java',
'util/INIParser.java',
'util/INISection.java',

View File

@ -5,6 +5,7 @@
package org.mozilla.gecko.preferences;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.R;
import org.mozilla.gecko.util.ThreadUtils;
import org.mozilla.gecko.RestrictedProfiles;
@ -26,7 +27,8 @@ class AndroidImportPreference extends MultiPrefMultiChoicePreference {
public static class Handler implements GeckoPreferences.PrefHandler {
public boolean setupPref(Context context, Preference pref) {
return RestrictedProfiles.isAllowed(context, Restriction.DISALLOW_IMPORT_SETTINGS);
// Feature disabled on devices running Android M+ (Bug 1183559)
return Versions.preM && RestrictedProfiles.isAllowed(context, Restriction.DISALLOW_IMPORT_SETTINGS);
}
public void onChange(Context context, Preference pref, Object newValue) { }

View File

@ -831,7 +831,7 @@ OnSharedPreferenceChangeListener
continue;
}
} else if (PREFS_QRCODE_ENABLED.equals(key)) {
if (!AppConstants.NIGHTLY_BUILD || !InputOptionsUtils.supportsQrCodeReader(getApplicationContext())) {
if (!InputOptionsUtils.supportsQrCodeReader(getApplicationContext())) {
// Remove UI for qr code input on non nightly builds
preferences.removePreference(pref);
i--;

View File

@ -253,10 +253,6 @@ public class ToolbarEditLayout extends ThemedLinearLayout {
}
private boolean qrCodeIsEnabled(Context context) {
// QR code is enabled for nightly only
if(!AppConstants.NIGHTLY_BUILD) {
return false;
}
final boolean qrCodeIsSupported = InputOptionsUtils.supportsQrCodeReader(context);
if (!qrCodeIsSupported) {
return false;

View File

@ -0,0 +1,143 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* * This Source Code Form is subject to the terms of the Mozilla Public
* * License, v. 2.0. If a copy of the MPL was not distributed with this
* * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.util;
import org.mozilla.gecko.AppConstants.Versions;
import android.media.MediaCodecInfo;
import android.media.MediaCodecInfo.CodecCapabilities;
import android.media.MediaCodecList;
import android.util.Log;
public final class HardwareCodecCapabilityUtils {
private static final String LOGTAG = "GeckoHardwareCodecCapabilityUtils";
// List of supported HW VP8 encoders.
private static final String[] supportedVp8HwEncCodecPrefixes =
{"OMX.qcom.", "OMX.Intel." };
// List of supported HW VP8 decoders.
private static final String[] supportedVp8HwDecCodecPrefixes =
{"OMX.qcom.", "OMX.Nvidia.", "OMX.Exynos.", "OMX.Intel." };
private static final String VP8_MIME_TYPE = "video/x-vnd.on2.vp8";
// NV12 color format supported by QCOM codec, but not declared in MediaCodec -
// see /hardware/qcom/media/mm-core/inc/OMX_QCOMExtns.h
private static final int
COLOR_QCOM_FORMATYUV420PackedSemiPlanar32m = 0x7FA30C04;
// Allowable color formats supported by codec - in order of preference.
private static final int[] supportedColorList = {
CodecCapabilities.COLOR_FormatYUV420Planar,
CodecCapabilities.COLOR_FormatYUV420SemiPlanar,
CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar,
COLOR_QCOM_FORMATYUV420PackedSemiPlanar32m
};
public static boolean getHWEncoderCapability() {
if (Versions.feature20Plus) {
for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) {
MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
if (!info.isEncoder()) {
continue;
}
String name = null;
for (String mimeType : info.getSupportedTypes()) {
if (mimeType.equals(VP8_MIME_TYPE)) {
name = info.getName();
break;
}
}
if (name == null) {
continue; // No HW support in this codec; try the next one.
}
Log.e(LOGTAG, "Found candidate encoder " + name);
// Check if this is supported encoder.
boolean supportedCodec = false;
for (String codecPrefix : supportedVp8HwEncCodecPrefixes) {
if (name.startsWith(codecPrefix)) {
supportedCodec = true;
break;
}
}
if (!supportedCodec) {
continue;
}
// Check if codec supports either yuv420 or nv12.
CodecCapabilities capabilities =
info.getCapabilitiesForType(VP8_MIME_TYPE);
for (int colorFormat : capabilities.colorFormats) {
Log.v(LOGTAG, " Color: 0x" + Integer.toHexString(colorFormat));
}
for (int supportedColorFormat : supportedColorList) {
for (int codecColorFormat : capabilities.colorFormats) {
if (codecColorFormat == supportedColorFormat) {
// Found supported HW Encoder.
Log.e(LOGTAG, "Found target encoder " + name +
". Color: 0x" + Integer.toHexString(codecColorFormat));
return true;
}
}
}
}
}
// No HW encoder.
return false;
}
public static boolean getHWDecoderCapability() {
if (Versions.feature20Plus) {
for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) {
MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
if (info.isEncoder()) {
continue;
}
String name = null;
for (String mimeType : info.getSupportedTypes()) {
if (mimeType.equals(VP8_MIME_TYPE)) {
name = info.getName();
break;
}
}
if (name == null) {
continue; // No HW support in this codec; try the next one.
}
Log.e(LOGTAG, "Found candidate decoder " + name);
// Check if this is supported decoder.
boolean supportedCodec = false;
for (String codecPrefix : supportedVp8HwDecCodecPrefixes) {
if (name.startsWith(codecPrefix)) {
supportedCodec = true;
break;
}
}
if (!supportedCodec) {
continue;
}
// Check if codec supports either yuv420 or nv12.
CodecCapabilities capabilities =
info.getCapabilitiesForType(VP8_MIME_TYPE);
for (int colorFormat : capabilities.colorFormats) {
Log.v(LOGTAG, " Color: 0x" + Integer.toHexString(colorFormat));
}
for (int supportedColorFormat : supportedColorList) {
for (int codecColorFormat : capabilities.colorFormats) {
if (codecColorFormat == supportedColorFormat) {
// Found supported HW decoder.
Log.e(LOGTAG, "Found target decoder " + name +
". Color: 0x" + Integer.toHexString(codecColorFormat));
return true;
}
}
}
}
}
return false; // No HW decoder.
}
}

View File

@ -38,7 +38,10 @@ public class InputOptionsUtils {
public static Intent createQRCodeReaderIntent() {
// Bug 602818 enables QR code input if you have the particular app below installed in your device
Intent intent = new Intent("com.google.zxing.client.android.SCAN");
final String appPackage = "com.google.zxing.client.android";
Intent intent = new Intent(appPackage + ".SCAN");
intent.setPackage(appPackage);
intent.putExtra("SCAN_MODE", "QR_CODE_MODE");
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
return intent;

View File

@ -218,7 +218,7 @@ let Logins = {
if ((newUsername === origUsername) &&
(newPassword === origPassword) &&
(newDomain === origDomain) ) {
gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("editLogin.saved"), "short");
gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("editLogin.saved1"), "short");
this._showList();
return;
}
@ -240,7 +240,7 @@ let Logins = {
gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("editLogin.couldNotSave"), "short");
return;
}
gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("editLogin.saved"), "short");
gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("editLogin.saved1"), "short");
this._showList();
},

View File

@ -1759,13 +1759,21 @@ var BrowserApp = {
// Add the current host in the 'trackingprotection' consumer of
// the permission manager using a normalized URI. This effectively
// places this host on the tracking protection white list.
Services.perms.add(normalizedUrl, "trackingprotection", Services.perms.ALLOW_ACTION);
if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
PrivateBrowsingUtils.addToTrackingAllowlist(normalizedUrl);
} else {
Services.perms.add(normalizedUrl, "trackingprotection", Services.perms.ALLOW_ACTION);
}
Telemetry.addData("TRACKING_PROTECTION_EVENTS", 1);
} else {
// Remove the current host from the 'trackingprotection' consumer
// of the permission manager. This effectively removes this host
// from the tracking protection white list (any list actually).
Services.perms.remove(normalizedUrl, "trackingprotection");
if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
PrivateBrowsingUtils.removeFromTrackingAllowlist(normalizedUrl);
} else {
Services.perms.remove(normalizedUrl, "trackingprotection");
}
Telemetry.addData("TRACKING_PROTECTION_EVENTS", 2);
}
}

View File

@ -480,6 +480,7 @@
@BINPATH@/components/nsUrlClassifierHashCompleter.js
@BINPATH@/components/nsUrlClassifierListManager.js
@BINPATH@/components/nsUrlClassifierLib.js
@BINPATH@/components/PrivateBrowsingTrackingProtectionWhitelist.js
@BINPATH@/components/url-classifier.xpt
#endif

View File

@ -14,7 +14,7 @@ loginsDialog.confirm=OK
loginsDialog.cancel=Cancel
editLogin.fallbackTitle=Edit Login
editLogin.saved=Saved login
editLogin.saved1=Saved login
editLogin.couldNotSave=Changes could not be saved
loginsDetails.age=Age: %S days

View File

@ -134,6 +134,8 @@ public class StringHelper {
public final String SCROLL_TITLE_BAR_LABEL;
public final String VOICE_INPUT_TITLE_LABEL;
public final String VOICE_INPUT_SUMMARY_LABEL;
public final String QRCODE_INPUT_TITLE_LABEL;
public final String QRCODE_INPUT_SUMMARY_LABEL;
public final String TEXT_REFLOW_LABEL;
public final String CHARACTER_ENCODING_LABEL;
public final String PLUGINS_LABEL;
@ -323,6 +325,8 @@ public class StringHelper {
SCROLL_TITLE_BAR_LABEL = res.getString(R.string.pref_scroll_title_bar2);
VOICE_INPUT_TITLE_LABEL = res.getString(R.string.pref_voice_input);
VOICE_INPUT_SUMMARY_LABEL = res.getString(R.string.pref_voice_input_summary);
QRCODE_INPUT_TITLE_LABEL = res.getString(R.string.pref_qrcode_enabled);
QRCODE_INPUT_SUMMARY_LABEL = res.getString(R.string.pref_qrcode_enabled_summary);
TEXT_REFLOW_LABEL = res.getString(R.string.pref_reflow_on_zoom);
CHARACTER_ENCODING_LABEL = res.getString(R.string.pref_char_encoding);
PLUGINS_LABEL = res.getString(R.string.pref_plugins);

View File

@ -233,6 +233,12 @@ public class testSettingsMenuItems extends PixelTest {
String[] voiceInputUi = { mStringHelper.VOICE_INPUT_TITLE_LABEL, mStringHelper.VOICE_INPUT_SUMMARY_LABEL };
settingsMap.get(PATH_DISPLAY).add(voiceInputUi);
}
// QR Code input
if (InputOptionsUtils.supportsQrCodeReader(this.getActivity().getApplicationContext())) {
String[] qrCodeInputUi = { mStringHelper.QRCODE_INPUT_TITLE_LABEL, mStringHelper.QRCODE_INPUT_SUMMARY_LABEL };
settingsMap.get(PATH_DISPLAY).add(qrCodeInputUi);
}
}
public void checkMenuHierarchy(Map<String[], List<String[]>> settingsMap) {

View File

@ -19,6 +19,7 @@
#include "nsIIOService.h"
#include "nsIParentChannel.h"
#include "nsIPermissionManager.h"
#include "nsIPrivateBrowsingTrackingProtectionWhitelist.h"
#include "nsIProtocolHandler.h"
#include "nsIScriptError.h"
#include "nsIScriptSecurityManager.h"
@ -163,6 +164,25 @@ nsChannelClassifier::ShouldEnableTrackingProtection(nsIChannel *aChannel,
*result = true;
}
// In Private Browsing Mode we also check against an in-memory list.
if (NS_UsePrivateBrowsing(aChannel)) {
nsCOMPtr<nsIPrivateBrowsingTrackingProtectionWhitelist> pbmtpWhitelist =
do_GetService(NS_PBTRACKINGPROTECTIONWHITELIST_CONTRACTID, &rv);
NS_ENSURE_SUCCESS(rv, rv);
bool exists = false;
rv = pbmtpWhitelist->ExistsInAllowList(topWinURI, &exists);
NS_ENSURE_SUCCESS(rv, rv);
if (exists) {
mIsAllowListed = true;
LOG(("nsChannelClassifier[%p]: Allowlisting channel[%p] in PBM for %s",
this, aChannel, escaped.get()));
}
*result = !exists;
}
// Tracking protection will be enabled so return without updating
// the security state. If any channels are subsequently cancelled
// (page elements blocked) the state will be then updated.

View File

@ -72,8 +72,7 @@ let publicProperties = [
// }
// If the state has changed between the function being called and the promise
// being resolved, the .resolve() call will actually be rejected.
let AccountState = this.AccountState = function(fxaInternal, storageManager) {
this.fxaInternal = fxaInternal;
let AccountState = this.AccountState = function(storageManager) {
this.storageManager = storageManager;
this.promiseInitialized = this.storageManager.getAccountData().then(data => {
this.oauthTokens = data && data.oauthTokens ? data.oauthTokens : {};
@ -84,13 +83,12 @@ let AccountState = this.AccountState = function(fxaInternal, storageManager) {
};
AccountState.prototype = {
cert: null,
keyPair: null,
oauthTokens: null,
whenVerifiedDeferred: null,
whenKeysReadyDeferred: null,
get isCurrent() this.fxaInternal && this.fxaInternal.currentAccountState === this,
// If the storage manager has been nuked then we are no longer current.
get isCurrent() this.storageManager != null,
abort() {
if (this.whenVerifiedDeferred) {
@ -108,7 +106,6 @@ AccountState.prototype = {
this.cert = null;
this.keyPair = null;
this.oauthTokens = null;
this.fxaInternal = null;
// Avoid finalizing the storageManager multiple times (ie, .signOut()
// followed by .abort())
if (!this.storageManager) {
@ -131,11 +128,14 @@ AccountState.prototype = {
});
},
getUserAccountData() {
// Get user account data. Optionally specify explcit field names to fetch
// (and note that if you require an in-memory field you *must* specify the
// field name(s).)
getUserAccountData(fieldNames = null) {
if (!this.isCurrent) {
return Promise.reject(new Error("Another user has signed in"));
}
return this.storageManager.getAccountData().then(result => {
return this.storageManager.getAccountData(fieldNames).then(result => {
return this.resolve(result);
});
},
@ -147,66 +147,6 @@ AccountState.prototype = {
return this.storageManager.updateAccountData(updatedFields);
},
getCertificate: function(data, keyPair, mustBeValidUntil) {
// TODO: get the lifetime from the cert's .exp field
if (this.cert && this.cert.validUntil > mustBeValidUntil) {
log.debug(" getCertificate already had one");
return this.resolve(this.cert.cert);
}
if (Services.io.offline) {
return this.reject(new Error(ERROR_OFFLINE));
}
let willBeValidUntil = this.fxaInternal.now() + CERT_LIFETIME;
return this.fxaInternal.getCertificateSigned(data.sessionToken,
keyPair.serializedPublicKey,
CERT_LIFETIME).then(
cert => {
log.debug("getCertificate got a new one: " + !!cert);
this.cert = {
cert: cert,
validUntil: willBeValidUntil
};
return cert;
}
).then(result => this.resolve(result));
},
getKeyPair: function(mustBeValidUntil) {
// If the debugging pref to ignore cached authentication credentials is set for Sync,
// then don't use any cached key pair, i.e., generate a new one and get it signed.
// The purpose of this pref is to expedite any auth errors as the result of a
// expired or revoked FxA session token, e.g., from resetting or changing the FxA
// password.
let ignoreCachedAuthCredentials = false;
try {
ignoreCachedAuthCredentials = Services.prefs.getBoolPref("services.sync.debug.ignoreCachedAuthCredentials");
} catch(e) {
// Pref doesn't exist
}
if (!ignoreCachedAuthCredentials && this.keyPair && (this.keyPair.validUntil > mustBeValidUntil)) {
log.debug("getKeyPair: already have a keyPair");
return this.resolve(this.keyPair.keyPair);
}
// Otherwse, create a keypair and set validity limit.
let willBeValidUntil = this.fxaInternal.now() + KEY_LIFETIME;
let d = Promise.defer();
jwcrypto.generateKeyPair("DS160", (err, kp) => {
if (err) {
return this.reject(err);
}
this.keyPair = {
keyPair: kp,
validUntil: willBeValidUntil
};
log.debug("got keyPair");
delete this.cert;
d.resolve(this.keyPair.keyPair);
});
return d.promise.then(result => this.resolve(result));
},
resolve: function(result) {
if (!this.isCurrent) {
log.info("An accountState promise was resolved, but was actually rejected" +
@ -427,7 +367,7 @@ FxAccountsInternal.prototype = {
newAccountState(credentials) {
let storage = new FxAccountsStorageManager();
storage.initialize(credentials);
return new AccountState(this, storage);
return new AccountState(storage);
},
/**
@ -559,6 +499,52 @@ FxAccountsInternal.prototype = {
})
},
/**
* returns a promise that fires with the keypair.
*/
getKeyPair: Task.async(function* (mustBeValidUntil) {
// If the debugging pref to ignore cached authentication credentials is set for Sync,
// then don't use any cached key pair, i.e., generate a new one and get it signed.
// The purpose of this pref is to expedite any auth errors as the result of a
// expired or revoked FxA session token, e.g., from resetting or changing the FxA
// password.
let ignoreCachedAuthCredentials = false;
try {
ignoreCachedAuthCredentials = Services.prefs.getBoolPref("services.sync.debug.ignoreCachedAuthCredentials");
} catch(e) {
// Pref doesn't exist
}
let currentState = this.currentAccountState;
let accountData = yield currentState.getUserAccountData("keyPair");
if (!ignoreCachedAuthCredentials && accountData.keyPair && (accountData.keyPair.validUntil > mustBeValidUntil)) {
log.debug("getKeyPair: already have a keyPair");
return accountData.keyPair.keyPair;
}
// Otherwse, create a keypair and set validity limit.
let willBeValidUntil = this.now() + KEY_LIFETIME;
let kp = yield new Promise((resolve, reject) => {
jwcrypto.generateKeyPair("DS160", (err, kp) => {
if (err) {
return reject(err);
}
log.debug("got keyPair");
let toUpdate = {
keyPair: {
keyPair: kp,
validUntil: willBeValidUntil
},
cert: null
};
currentState.updateUserAccountData(toUpdate).then(() => {
resolve(kp);
}).catch(err => {
log.error("Failed to update account data with keypair and cert");
});
});
});
return kp;
}),
/**
* returns a promise that fires with the assertion. If there is no verified
* signed-in user, fires with null.
@ -576,8 +562,8 @@ FxAccountsInternal.prototype = {
// Signed-in user has not verified email
return null;
}
return currentState.getKeyPair(mustBeValidUntil).then(keyPair => {
return currentState.getCertificate(data, keyPair, mustBeValidUntil)
return this.getKeyPair(mustBeValidUntil).then(keyPair => {
return this.getCertificate(data, keyPair, mustBeValidUntil)
.then(cert => {
return this.getAssertionFromCert(data, keyPair, cert, audience);
});
@ -845,6 +831,37 @@ FxAccountsInternal.prototype = {
);
},
/**
* returns a promise that fires with a certificate.
*/
getCertificate: Task.async(function* (data, keyPair, mustBeValidUntil) {
// TODO: get the lifetime from the cert's .exp field
let currentState = this.currentAccountState;
let accountData = yield currentState.getUserAccountData("cert");
if (accountData.cert && accountData.cert.validUntil > mustBeValidUntil) {
log.debug(" getCertificate already had one");
return accountData.cert.cert;
}
if (Services.io.offline) {
throw new Error(ERROR_OFFLINE);
}
let willBeValidUntil = this.now() + CERT_LIFETIME;
let cert = yield this.getCertificateSigned(data.sessionToken,
keyPair.serializedPublicKey,
CERT_LIFETIME);
log.debug("getCertificate got a new one: " + !!cert);
if (cert) {
let toUpdate = {
cert: {
cert: cert,
validUntil: willBeValidUntil
}
};
yield currentState.updateUserAccountData(toUpdate);
}
return cert;
}),
getUserAccountData: function() {
return this.currentAccountState.getUserAccountData();
},

View File

@ -212,13 +212,22 @@ exports.ERROR_MSG_METHOD_NOT_ALLOWED = "METHOD_NOT_ALLOWED";
// FxAccounts has the ability to "split" the credentials between a plain-text
// JSON file in the profile dir and in the login manager.
// These constants relate to that.
// In order to prevent new fields accidentally ending up in the "wrong" place,
// all fields stored are listed here.
// The fields we save in the plaintext JSON.
// See bug 1013064 comments 23-25 for why the sessionToken is "safe"
exports.FXA_PWDMGR_PLAINTEXT_FIELDS = ["email", "verified", "authAt",
"sessionToken", "uid", "oauthTokens",
"profile"];
exports.FXA_PWDMGR_PLAINTEXT_FIELDS = new Set(
["email", "verified", "authAt", "sessionToken", "uid", "oauthTokens", "profile"]);
// Fields we store in secure storage if it exists.
exports.FXA_PWDMGR_SECURE_FIELDS = new Set(
["kA", "kB", "keyFetchToken", "unwrapBKey", "assertion"]);
// Fields we keep in memory and don't persist anywhere.
exports.FXA_PWDMGR_MEMORY_FIELDS = new Set(
["cert", "keyPair"]);
// The pseudo-host we use in the login manager
exports.FXA_PWDMGR_HOST = "chrome://FirefoxAccounts";
// The realm we use in the login manager.

View File

@ -62,10 +62,18 @@ this.FxAccountsStorageManager.prototype = {
this._needToReadSecure = false;
// split it into the 2 parts, write it and we are done.
for (let [name, val] of Iterator(accountData)) {
if (FXA_PWDMGR_PLAINTEXT_FIELDS.indexOf(name) >= 0) {
if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(name)) {
this.cachedPlain[name] = val;
} else {
} else if (FXA_PWDMGR_SECURE_FIELDS.has(name)) {
this.cachedSecure[name] = val;
} else {
// Hopefully it's an "in memory" field. If it's not we log a warning
// but still treat it as such (so it will still be available in this
// session but isn't persisted anywhere.)
if (!FXA_PWDMGR_MEMORY_FIELDS.has(name)) {
log.warn("Unknown FxA field name in user data, treating as in-memory", name);
}
this.cachedMemory[name] = val;
}
}
// write it out and we are done.
@ -121,7 +129,12 @@ this.FxAccountsStorageManager.prototype = {
},
// Get the account data by combining the plain and secure storage.
getAccountData: Task.async(function* () {
// If fieldNames is specified, it may be a string or an array of strings,
// and only those fields are returned. If not specified the entire account
// data is returned except for "in memory" fields. Note that not specifying
// field names will soon be deprecated/removed - we want all callers to
// specify the fields they care about.
getAccountData: Task.async(function* (fieldNames = null) {
yield this._promiseInitialized;
// We know we are initialized - this means our .cachedPlain is accurate
// and doesn't need to be read (it was read if necessary by initialize).
@ -130,21 +143,53 @@ this.FxAccountsStorageManager.prototype = {
return null;
}
let result = {};
for (let [name, value] of Iterator(this.cachedPlain)) {
result[name] = value;
if (fieldNames === null) {
// The "old" deprecated way of fetching a logged in user.
for (let [name, value] of Iterator(this.cachedPlain)) {
result[name] = value;
}
// But the secure data may not have been read, so try that now.
yield this._maybeReadAndUpdateSecure();
// .cachedSecure now has as much as it possibly can (which is possibly
// nothing if (a) secure storage remains locked and (b) we've never updated
// a field to be stored in secure storage.)
for (let [name, value] of Iterator(this.cachedSecure)) {
result[name] = value;
}
// Note we don't return cachedMemory fields here - they must be explicitly
// requested.
return result;
}
// But the secure data may not have been read, so try that now.
yield this._maybeReadAndUpdateSecure();
// .cachedSecure now has as much as it possibly can (which is possibly
// nothing if (a) secure storage remains locked and (b) we've never updated
// a field to be stored in secure storage.)
for (let [name, value] of Iterator(this.cachedSecure)) {
result[name] = value;
// The new explicit way of getting attributes.
if (!Array.isArray(fieldNames)) {
fieldNames = [fieldNames];
}
let checkedSecure = false;
for (let fieldName of fieldNames) {
if (FXA_PWDMGR_MEMORY_FIELDS.has(fieldName)) {
if (this.cachedMemory[fieldName] !== undefined) {
result[fieldName] = this.cachedMemory[fieldName];
}
} else if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(fieldName)) {
if (this.cachedPlain[fieldName] !== undefined) {
result[fieldName] = this.cachedPlain[fieldName];
}
} else if (FXA_PWDMGR_SECURE_FIELDS.has(fieldName)) {
// We may not have read secure storage yet.
if (!checkedSecure) {
yield this._maybeReadAndUpdateSecure();
checkedSecure = true;
}
if (this.cachedSecure[fieldName] !== undefined) {
result[fieldName] = this.cachedSecure[fieldName];
}
} else {
throw new Error("unexpected field '" + name + "'");
}
}
return result;
}),
// Update just the specified fields. This DOES NOT allow you to change to
// a different user, nor to set the user as signed-out.
updateAccountData: Task.async(function* (newFields) {
@ -163,16 +208,27 @@ this.FxAccountsStorageManager.prototype = {
log.debug("_updateAccountData with items", Object.keys(newFields));
// work out what bucket.
for (let [name, value] of Iterator(newFields)) {
if (FXA_PWDMGR_PLAINTEXT_FIELDS.indexOf(name) >= 0) {
if (FXA_PWDMGR_MEMORY_FIELDS.has(name)) {
if (value == null) {
delete this.cachedMemory[name];
} else {
this.cachedMemory[name] = value;
}
} else if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(name)) {
if (value == null) {
delete this.cachedPlain[name];
} else {
this.cachedPlain[name] = value;
}
} else {
} else if (FXA_PWDMGR_SECURE_FIELDS.has(name)) {
// don't do the "delete on null" thing here - we need to keep it until
// we have managed to read so we can nuke it on write.
this.cachedSecure[name] = value;
} else {
// Throwing seems reasonable here as some client code has explicitly
// specified the field name, so it's either confused or needs to update
// how this field is to be treated.
throw new Error("unexpected field '" + name + "'");
}
}
// If we haven't yet read the secure data, do so now, else we may write
@ -185,6 +241,7 @@ this.FxAccountsStorageManager.prototype = {
}),
_clearCachedData() {
this.cachedMemory = {};
this.cachedPlain = {};
// If we don't have secure storage available we have cachedPlain and
// cachedSecure be the same object.

View File

@ -155,7 +155,7 @@ function MockFxAccounts() {
// we use a real accountState but mocked storage.
let storage = new MockStorageManager();
storage.initialize(credentials);
return new AccountState(this, storage);
return new AccountState(storage);
},
getCertificateSigned: function (sessionToken, serializedPublicKey) {
_("mock getCertificateSigned\n");
@ -202,7 +202,7 @@ add_task(function test_get_signed_in_user_initially_unset() {
// we use a real accountState but mocked storage.
let storage = new MockStorageManager();
storage.initialize(credentials);
return new AccountState(this, storage);
return new AccountState(storage);
},
});
let credentials = {
@ -251,7 +251,7 @@ add_task(function* test_getCertificate() {
// we use a real accountState but mocked storage.
let storage = new MockStorageManager();
storage.initialize(credentials);
return new AccountState(this, storage);
return new AccountState(storage);
},
});
let credentials = {
@ -272,7 +272,7 @@ add_task(function* test_getCertificate() {
let offline = Services.io.offline;
Services.io.offline = true;
// This call would break from missing parameters ...
yield fxa.internal.currentAccountState.getCertificate().then(
yield fxa.internal.getCertificate().then(
result => {
Services.io.offline = offline;
do_throw("Unexpected success");
@ -505,8 +505,9 @@ add_task(function test_getAssertion() {
_("ASSERTION: " + assertion + "\n");
let pieces = assertion.split("~");
do_check_eq(pieces[0], "cert1");
let keyPair = fxa.internal.currentAccountState.keyPair;
let cert = fxa.internal.currentAccountState.cert;
let userData = yield fxa.getSignedInUser();
let keyPair = userData.keyPair;
let cert = userData.cert;
do_check_neq(keyPair, undefined);
_(keyPair.validUntil + "\n");
let p2 = pieces[1].split(".");
@ -553,9 +554,10 @@ add_task(function test_getAssertion() {
// expiration time of the assertion should be different. We compare this to
// the initial start time, to which they are relative, not the current value
// of "now".
userData = yield fxa.getSignedInUser();
keyPair = fxa.internal.currentAccountState.keyPair;
cert = fxa.internal.currentAccountState.cert;
keyPair = userData.keyPair;
cert = userData.cert;
do_check_eq(keyPair.validUntil, start + KEY_LIFETIME);
do_check_eq(cert.validUntil, start + CERT_LIFETIME);
exp = Number(payload.exp);
@ -576,8 +578,9 @@ add_task(function test_getAssertion() {
header = JSON.parse(atob(p2[0]));
payload = JSON.parse(atob(p2[1]));
do_check_eq(payload.aud, "fourth.example.com");
keyPair = fxa.internal.currentAccountState.keyPair;
cert = fxa.internal.currentAccountState.cert;
userData = yield fxa.getSignedInUser();
keyPair = userData.keyPair;
cert = userData.cert;
do_check_eq(keyPair.validUntil, now + KEY_LIFETIME);
do_check_eq(cert.validUntil, now + CERT_LIFETIME);
exp = Number(payload.exp);

View File

@ -85,7 +85,7 @@ function MockFxAccounts() {
// we use a real accountState but mocked storage.
let storage = new MockStorageManager();
storage.initialize(credentials);
return new AccountState(this, storage);
return new AccountState(storage);
},
});
}

View File

@ -51,9 +51,11 @@ function MockedSecureStorage(accountData) {
}
MockedSecureStorage.prototype = {
fetchCount: 0,
locked: false,
STORAGE_LOCKED: function() {},
get: Task.async(function* (uid, email) {
this.fetchCount++;
if (this.locked) {
throw new this.STORAGE_LOCKED();
}
@ -85,7 +87,7 @@ add_storage_task(function* checkInitializedEmpty(sm) {
}
yield sm.initialize();
Assert.strictEqual((yield sm.getAccountData()), null);
Assert.rejects(sm.updateAccountData({foo: "bar"}), "No user is logged in")
Assert.rejects(sm.updateAccountData({kA: "kA"}), "No user is logged in")
});
// Initialized with account data (ie, simulating a new user being logged in).
@ -130,9 +132,9 @@ add_storage_task(function* checkEverythingRead(sm) {
Assert.equal(accountData.email, "someone@somewhere.com");
// Update the data - we should be able to fetch it back and it should appear
// in our storage.
yield sm.updateAccountData({verified: true, foo: "bar", kA: "kA"});
yield sm.updateAccountData({verified: true, kA: "kA", kB: "kB"});
accountData = yield sm.getAccountData();
Assert.equal(accountData.foo, "bar");
Assert.equal(accountData.kB, "kB");
Assert.equal(accountData.kA, "kA");
// Check the new value was written to storage.
yield sm._promiseStorageComplete; // storage is written in the background.
@ -141,10 +143,10 @@ add_storage_task(function* checkEverythingRead(sm) {
// "kA" and "foo" are secure
if (sm.secureStorage) {
Assert.equal(sm.secureStorage.data.accountData.kA, "kA");
Assert.equal(sm.secureStorage.data.accountData.foo, "bar");
Assert.equal(sm.secureStorage.data.accountData.kB, "kB");
} else {
Assert.equal(sm.plainStorage.data.accountData.kA, "kA");
Assert.equal(sm.plainStorage.data.accountData.foo, "bar");
Assert.equal(sm.plainStorage.data.accountData.kB, "kB");
}
});
@ -231,6 +233,53 @@ add_task(function* checkEverythingReadSecure() {
Assert.equal(accountData.kA, "kA");
});
add_task(function* checkMemoryFieldsNotReturnedByDefault() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"})
sm.secureStorage = new MockedSecureStorage({kA: "kA"});
yield sm.initialize();
// keyPair is a memory field.
yield sm.updateAccountData({keyPair: "the keypair value"});
let accountData = yield sm.getAccountData();
// Requesting everything should *not* return in memory fields.
Assert.strictEqual(accountData.keyPair, undefined);
// But requesting them specifically does get them.
accountData = yield sm.getAccountData("keyPair");
Assert.strictEqual(accountData.keyPair, "the keypair value");
});
add_task(function* checkExplicitGet() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"})
sm.secureStorage = new MockedSecureStorage({kA: "kA"});
yield sm.initialize();
let accountData = yield sm.getAccountData(["uid", "kA"]);
Assert.ok(accountData, "read account data");
Assert.equal(accountData.uid, "uid");
Assert.equal(accountData.kA, "kA");
// We didn't ask for email so shouldn't have got it.
Assert.strictEqual(accountData.email, undefined);
});
add_task(function* checkExplicitGetNoSecureRead() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"})
sm.secureStorage = new MockedSecureStorage({kA: "kA"});
yield sm.initialize();
Assert.equal(sm.secureStorage.fetchCount, 0);
// request 2 fields in secure storage - it should have caused a single fetch.
let accountData = yield sm.getAccountData(["email", "uid"]);
Assert.ok(accountData, "read account data");
Assert.equal(accountData.uid, "uid");
Assert.equal(accountData.email, "someone@somewhere.com");
Assert.strictEqual(accountData.kA, undefined);
Assert.equal(sm.secureStorage.fetchCount, 1);
});
add_task(function* checkLockedUpdates() {
let sm = new FxAccountsStorageManager();
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"})

View File

@ -181,17 +181,17 @@ this.configureFxAccountIdentity = function(authService,
}
let storageManager = new MockFxaStorageManager();
storageManager.initialize(config.fxaccount.user);
let accountState = new AccountState(this, storageManager);
// mock getCertificate
accountState.getCertificate = function(data, keyPair, mustBeValidUntil) {
accountState.cert = {
validUntil: fxa.internal.now() + CERT_LIFETIME,
cert: "certificate",
};
return Promise.resolve(this.cert.cert);
}
let accountState = new AccountState(storageManager);
return accountState;
}
},
getCertificate(data, keyPair, mustBeValidUntil) {
let cert = {
validUntil: this.now() + CERT_LIFETIME,
cert: "certificate",
};
this.currentAccountState.updateUserAccountData({cert: cert});
return Promise.resolve(cert.cert);
},
};
fxa = new FxAccounts(MockInternal);

View File

@ -595,7 +595,7 @@ add_task(function test_getKeysMissing() {
}
let storageManager = new MockFxaStorageManager();
storageManager.initialize(identityConfig.fxaccount.user);
return new AccountState(this, storageManager);
return new AccountState(storageManager);
},
});
@ -674,7 +674,7 @@ function* initializeIdentityWithHAWKResponseFactory(config, cbGetResponse) {
}
let storageManager = new MockFxaStorageManager();
storageManager.initialize(config.fxaccount.user);
return new AccountState(this, storageManager);
return new AccountState(storageManager);
},
}
let fxa = new FxAccounts(internal);

View File

@ -348,7 +348,7 @@ OptimizeIconSize(IconData& aIcon,
AsyncFaviconHelperBase::AsyncFaviconHelperBase(
nsCOMPtr<nsIFaviconDataCallback>& aCallback
) : mDB(Database::GetDatabase())
)
{
// Don't AddRef or Release in runnables for thread-safety.
mCallback.swap(aCallback);
@ -451,7 +451,9 @@ AsyncFetchAndSetIconForPage::Run()
"This should not be called on the main thread");
// Try to fetch the icon from the database.
nsresult rv = FetchIconInfo(mDB, mIcon);
nsRefPtr<Database> DB = Database::GetDatabase();
NS_ENSURE_STATE(DB);
nsresult rv = FetchIconInfo(DB, mIcon);
NS_ENSURE_SUCCESS(rv, rv);
bool isInvalidIcon = mIcon.data.IsEmpty() ||
@ -464,7 +466,7 @@ AsyncFetchAndSetIconForPage::Run()
// directly proceed with association.
nsRefPtr<AsyncAssociateIconToPage> event =
new AsyncAssociateIconToPage(mIcon, mPage, mCallback);
mDB->DispatchToAsyncThread(event);
DB->DispatchToAsyncThread(event);
return NS_OK;
}
@ -695,9 +697,11 @@ AsyncFetchAndSetIconFromNetwork::OnStopRequest(nsIRequest* aRequest,
mIcon.status = ICON_STATUS_CHANGED;
nsRefPtr<Database> DB = Database::GetDatabase();
NS_ENSURE_STATE(DB);
nsRefPtr<AsyncAssociateIconToPage> event =
new AsyncAssociateIconToPage(mIcon, mPage, mCallback);
mDB->DispatchToAsyncThread(event);
DB->DispatchToAsyncThread(event);
return NS_OK;
}
@ -725,7 +729,9 @@ AsyncAssociateIconToPage::Run()
NS_PRECONDITION(!NS_IsMainThread(),
"This should not be called on the main thread");
nsresult rv = FetchPageInfo(mDB, mPage);
nsRefPtr<Database> DB = Database::GetDatabase();
NS_ENSURE_STATE(DB);
nsresult rv = FetchPageInfo(DB, mPage);
if (rv == NS_ERROR_NOT_AVAILABLE){
// We have never seen this page. If we can add the page to history,
// we will try to do it later, otherwise just bail out.
@ -737,19 +743,19 @@ AsyncAssociateIconToPage::Run()
NS_ENSURE_SUCCESS(rv, rv);
}
mozStorageTransaction transaction(mDB->MainConn(), false,
mozStorageTransaction transaction(DB->MainConn(), false,
mozIStorageConnection::TRANSACTION_IMMEDIATE);
// If there is no entry for this icon, or the entry is obsolete, replace it.
if (mIcon.id == 0 || (mIcon.status & ICON_STATUS_CHANGED)) {
rv = SetIconInfo(mDB, mIcon);
rv = SetIconInfo(DB, mIcon);
NS_ENSURE_SUCCESS(rv, rv);
// Get the new icon id. Do this regardless mIcon.id, since other code
// could have added a entry before us. Indeed we interrupted the thread
// after the previous call to FetchIconInfo.
mIcon.status = (mIcon.status & ~(ICON_STATUS_CACHED)) | ICON_STATUS_SAVED;
rv = FetchIconInfo(mDB, mIcon);
rv = FetchIconInfo(DB, mIcon);
NS_ENSURE_SUCCESS(rv, rv);
}
@ -764,7 +770,7 @@ AsyncAssociateIconToPage::Run()
if (mPage.iconId != mIcon.id) {
nsCOMPtr<mozIStorageStatement> stmt;
if (mPage.id) {
stmt = mDB->GetStatement(
stmt = DB->GetStatement(
"UPDATE moz_places SET favicon_id = :icon_id WHERE id = :page_id"
);
NS_ENSURE_STATE(stmt);
@ -772,7 +778,7 @@ AsyncAssociateIconToPage::Run()
NS_ENSURE_SUCCESS(rv, rv);
}
else {
stmt = mDB->GetStatement(
stmt = DB->GetStatement(
"UPDATE moz_places SET favicon_id = :icon_id WHERE url = :page_url"
);
NS_ENSURE_STATE(stmt);
@ -846,8 +852,10 @@ AsyncGetFaviconURLForPage::Run()
NS_PRECONDITION(!NS_IsMainThread(),
"This should not be called on the main thread.");
nsRefPtr<Database> DB = Database::GetDatabase();
NS_ENSURE_STATE(DB);
nsAutoCString iconSpec;
nsresult rv = FetchIconURL(mDB, mPageSpec, iconSpec);
nsresult rv = FetchIconURL(DB, mPageSpec, iconSpec);
NS_ENSURE_SUCCESS(rv, rv);
// Now notify our callback of the icon spec we retrieved, even if empty.
@ -911,8 +919,10 @@ AsyncGetFaviconDataForPage::Run()
NS_PRECONDITION(!NS_IsMainThread(),
"This should not be called on the main thread.");
nsRefPtr<Database> DB = Database::GetDatabase();
NS_ENSURE_STATE(DB);
nsAutoCString iconSpec;
nsresult rv = FetchIconURL(mDB, mPageSpec, iconSpec);
nsresult rv = FetchIconURL(DB, mPageSpec, iconSpec);
NS_ENSURE_SUCCESS(rv, rv);
IconData iconData;
@ -922,7 +932,7 @@ AsyncGetFaviconDataForPage::Run()
pageData.spec.Assign(mPageSpec);
if (!iconSpec.IsEmpty()) {
rv = FetchIconInfo(mDB, iconData);
rv = FetchIconInfo(DB, iconData);
if (NS_FAILED(rv)) {
iconData.spec.Truncate();
}
@ -975,16 +985,18 @@ AsyncReplaceFaviconData::Run()
NS_PRECONDITION(!NS_IsMainThread(),
"This should not be called on the main thread");
nsRefPtr<Database> DB = Database::GetDatabase();
NS_ENSURE_STATE(DB);
IconData dbIcon;
dbIcon.spec.Assign(mIcon.spec);
nsresult rv = FetchIconInfo(mDB, dbIcon);
nsresult rv = FetchIconInfo(DB, dbIcon);
NS_ENSURE_SUCCESS(rv, rv);
if (!dbIcon.id) {
return NS_OK;
}
rv = SetIconInfo(mDB, mIcon);
rv = SetIconInfo(DB, mIcon);
NS_ENSURE_SUCCESS(rv, rv);
// We can invalidate the cache version since we now persist the icon.
@ -1096,11 +1108,14 @@ NotifyIconObservers::SendGlobalNotifications(nsIURI* aIconURI)
PageData bookmarkedPage;
bookmarkedPage.spec = mPage.bookmarkedSpec;
nsRefPtr<Database> DB = Database::GetDatabase();
if (!DB)
return;
// This will be silent, so be sure to not pass in the current callback.
nsCOMPtr<nsIFaviconDataCallback> nullCallback;
nsRefPtr<AsyncAssociateIconToPage> event =
new AsyncAssociateIconToPage(mIcon, bookmarkedPage, nullCallback);
mDB->DispatchToAsyncThread(event);
DB->DispatchToAsyncThread(event);
}
}

View File

@ -102,7 +102,6 @@ protected:
virtual ~AsyncFaviconHelperBase();
nsRefPtr<Database> mDB;
// Strong reference since we are responsible for its existence.
nsCOMPtr<nsIFaviconDataCallback> mCallback;
};

View File

@ -315,8 +315,8 @@ public:
* `true` if we have not started shutdown, i.e. if
* `BlockShutdown()` hasn't been called yet, false otherwise.
*/
bool IsStarted() const {
return mIsStarted;
static bool IsStarted() {
return sIsStarted;
}
private:
@ -354,21 +354,22 @@ private:
NOTIFIED_OBSERVERS_PLACES_CONNECTION_CLOSED,
};
State mState;
bool mIsStarted;
// As tests may resurrect a dead `Database`, we use a counter to
// give the instances of `DatabaseShutdown` unique names.
uint16_t mCounter;
static uint16_t sCounter;
static Atomic<bool> sIsStarted;
~DatabaseShutdown() {}
};
uint16_t DatabaseShutdown::sCounter = 0;
Atomic<bool> DatabaseShutdown::sIsStarted(false);
DatabaseShutdown::DatabaseShutdown(Database* aDatabase)
: mDatabase(aDatabase)
, mState(NOT_STARTED)
, mIsStarted(false)
, mCounter(sCounter++)
{
MOZ_ASSERT(NS_IsMainThread());
@ -465,7 +466,7 @@ DatabaseShutdown::BlockShutdown(nsIAsyncShutdownClient* aParentClient)
{
mParentClient = aParentClient;
mState = RECEIVED_BLOCK_SHUTDOWN;
mIsStarted = true;
sIsStarted = true;
if (NS_WARN_IF(!mBarrier)) {
return NS_ERROR_NOT_AVAILABLE;
@ -662,6 +663,15 @@ Database::GetConnectionShutdown()
return mConnectionShutdown->GetClient();
}
// static
already_AddRefed<Database>
Database::GetDatabase()
{
if (DatabaseShutdown::IsStarted())
return nullptr;
return GetSingleton();
}
nsresult
Database::Init()
{

View File

@ -94,10 +94,7 @@ public:
*
* @return Singleton instance of this class.
*/
static already_AddRefed<Database> GetDatabase()
{
return GetSingleton();
}
static already_AddRefed<Database> GetDatabase();
/**
* Returns last known database status.

View File

@ -146,7 +146,6 @@ function gzipCompressString(string) {
return observer.buffer;
}
this.TelemetrySend = {
/**
@ -391,8 +390,12 @@ let SendScheduler = {
let pending = TelemetryStorage.getPendingPingList();
let current = TelemetrySendImpl.getUnpersistedPings();
this._log.trace("_doSendTask - pending: " + pending.length + ", current: " + current.length);
pending = pending.filter(p => TelemetrySendImpl.sendingEnabled(p));
current = current.filter(p => TelemetrySendImpl.sendingEnabled(p));
// Note that the two lists contain different kind of data. |pending| only holds ping
// info, while |current| holds actual ping data.
if (!TelemetrySendImpl.sendingEnabled()) {
pending = pending.filter(pingInfo => TelemetryStorage.isDeletionPing(pingInfo.id));
current = current.filter(p => isDeletionPing(p));
}
this._log.trace("_doSendTask - can send - pending: " + pending.length + ", current: " + current.length);
// Bail out if there is nothing to send.
@ -712,7 +715,12 @@ let TelemetrySendImpl = {
yield this._doPing(ping, ping.id, false);
} catch (ex) {
this._log.info("sendPings - ping " + ping.id + " not sent, saving to disk", ex);
yield TelemetryStorage.savePendingPing(ping);
// Deletion pings must be saved to a special location.
if (isDeletionPing(ping)) {
yield TelemetryStorage.saveDeletionPing(ping);
} else {
yield TelemetryStorage.savePendingPing(ping);
}
} finally {
this._currentPings.delete(ping.id);
}
@ -784,6 +792,9 @@ let TelemetrySendImpl = {
}
if (success && isPersisted) {
if (TelemetryStorage.isDeletionPing(id)) {
return TelemetryStorage.removeDeletionPing();
}
return TelemetryStorage.removePendingPing(id);
} else {
return Promise.resolve();

View File

@ -34,6 +34,7 @@ const Utils = TelemetryUtils;
const DATAREPORTING_DIR = "datareporting";
const PINGS_ARCHIVE_DIR = "archived";
const ABORTED_SESSION_FILE_NAME = "aborted-session-ping";
const DELETION_PING_FILE_NAME = "pending-deletion-ping";
XPCOMUtils.defineLazyGetter(this, "gDataReportingDir", function() {
return OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIR);
@ -44,6 +45,9 @@ XPCOMUtils.defineLazyGetter(this, "gPingsArchivePath", function() {
XPCOMUtils.defineLazyGetter(this, "gAbortedSessionFilePath", function() {
return OS.Path.join(gDataReportingDir, ABORTED_SESSION_FILE_NAME);
});
XPCOMUtils.defineLazyGetter(this, "gDeletionPingFilePath", function() {
return OS.Path.join(gDataReportingDir, DELETION_PING_FILE_NAME);
});
// Maxmimum time, in milliseconds, archive pings should be retained.
const MAX_ARCHIVED_PINGS_RETENTION_MS = 180 * 24 * 60 * 60 * 1000; // 180 days
@ -262,6 +266,30 @@ this.TelemetryStorage = {
return TelemetryStorageImpl.loadAbortedSessionPing();
},
/**
* Save the deletion ping.
* @param ping The deletion ping.
* @return {Promise} A promise resolved when the ping is saved.
*/
saveDeletionPing: function(ping) {
return TelemetryStorageImpl.saveDeletionPing(ping);
},
/**
* Remove the deletion ping.
* @return {Promise} Resolved when the ping is deleted from the disk.
*/
removeDeletionPing: function() {
return TelemetryStorageImpl.removeDeletionPing();
},
/**
* Check if the ping id identifies a deletion ping.
*/
isDeletionPing: function(aPingId) {
return TelemetryStorageImpl.isDeletionPing(aPingId);
},
/**
* Remove the aborted-session ping if present.
*
@ -482,6 +510,8 @@ let TelemetryStorageImpl = {
_logger: null,
// Used to serialize aborted session ping writes to disk.
_abortedSessionSerializer: new SaveSerializer(),
// Used to serialize deletion ping writes to disk.
_deletionPingSerializer: new SaveSerializer(),
// Tracks the archived pings in a Map of (id -> {timestampCreated, type}).
// We use this to cache info on archived pings to avoid scanning the disk more than once.
@ -521,6 +551,7 @@ let TelemetryStorageImpl = {
shutdown: Task.async(function*() {
this._shutdown = true;
yield this._abortedSessionSerializer.flushTasks();
yield this._deletionPingSerializer.flushTasks();
// If the tasks for archive cleaning or pending ping quota are still running, block on
// them. They will bail out as soon as possible.
yield this._cleanArchiveTask;
@ -1225,6 +1256,18 @@ let TelemetryStorageImpl = {
}
yield iter.close();
// Explicitly load the deletion ping from its known path, if it's there.
if (yield OS.File.exists(gDeletionPingFilePath)) {
this._log.trace("_scanPendingPings - Adding pending deletion ping.");
// We can't get the ping id or the last modification date without hitting the disk.
// Since deletion has a special handling, we don't really need those.
this._pendingPings.set(Utils.generateUUID(), {
path: gDeletionPingFilePath,
lastModificationDate: Date.now(),
});
}
this._scannedPendingDirectory = true;
return this._buildPingList();
}),
@ -1379,6 +1422,50 @@ let TelemetryStorageImpl = {
}
}.bind(this)));
},
/**
* Save the deletion ping.
* @param ping The deletion ping.
* @return {Promise} Resolved when the ping is saved.
*/
saveDeletionPing: Task.async(function*(ping) {
this._log.trace("saveDeletionPing - ping path: " + gDeletionPingFilePath);
yield OS.File.makeDir(gDataReportingDir, { ignoreExisting: true });
return this._deletionPingSerializer.enqueueTask(() =>
this.savePingToFile(ping, gDeletionPingFilePath, true));
}),
/**
* Remove the deletion ping.
* @return {Promise} Resolved when the ping is deleted from the disk.
*/
removeDeletionPing: Task.async(function*() {
return this._deletionPingSerializer.enqueueTask(Task.async(function*() {
try {
yield OS.File.remove(gDeletionPingFilePath, { ignoreAbsent: false });
this._log.trace("removeDeletionPing - success");
} catch (ex if ex.becauseNoSuchFile) {
this._log.trace("removeDeletionPing - no such file");
} catch (ex) {
this._log.error("removeDeletionPing - error removing ping", ex)
}
}.bind(this)));
}),
isDeletionPing: function(aPingId) {
this._log.trace("isDeletionPing - id: " + aPingId);
let pingInfo = this._pendingPings.get(aPingId);
if (!pingInfo) {
return false;
}
if (pingInfo.path != gDeletionPingFilePath) {
return false;
}
return true;
},
};
///// Utility functions

View File

@ -152,11 +152,37 @@ add_task(function* test_deletionPing() {
return;
}
const PREF_TELEMETRY_SERVER = "toolkit.telemetry.server";
// Disable FHR upload: this should trigger a deletion ping.
Preferences.set(PREF_FHR_UPLOAD_ENABLED, false);
let ping = yield PingServer.promiseNextPing();
checkPingFormat(ping, DELETION_PING_TYPE, true, false);
// Wait on ping activity to settle.
yield TelemetrySend.testWaitOnOutgoingPings();
// Restore FHR Upload.
Preferences.set(PREF_FHR_UPLOAD_ENABLED, true);
// Simulate a failure in sending the deletion ping by disabling the HTTP server.
yield PingServer.stop();
// Disable FHR upload to send a deletion ping again.
Preferences.set(PREF_FHR_UPLOAD_ENABLED, false);
// Wait for the send task to terminate, flagging it to do so at the next opportunity and
// cancelling any timeouts.
yield TelemetryController.reset();
// Enable the ping server again.
PingServer.start();
// We set the new server using the pref, otherwise it would get reset with
// |TelemetryController.reset|.
Preferences.set(PREF_TELEMETRY_SERVER, "http://localhost:" + PingServer.port);
// Reset the controller to spin the ping sending task.
yield TelemetryController.reset();
ping = yield PingServer.promiseNextPing();
checkPingFormat(ping, DELETION_PING_TYPE, true, false);
// Restore FHR Upload.
Preferences.set(PREF_FHR_UPLOAD_ENABLED, true);
@ -262,7 +288,7 @@ add_task(function* test_archivePings() {
add_task(function* test_midnightPingSendFuzzing() {
const fuzzingDelay = 60 * 60 * 1000;
fakeMidnightPingFuzzingDelay(fuzzingDelay);
let now = new Date(2030, 5, 1, 11, 00, 0);
let now = new Date(2030, 5, 1, 11, 0, 0);
fakeNow(now);
let waitForTimer = () => new Promise(resolve => {

View File

@ -0,0 +1,68 @@
/* 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/. */
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
function PrivateBrowsingTrackingProtectionWhitelist() {
// The list of URIs explicitly excluded from tracking protection.
this._allowlist = [];
Services.obs.addObserver(this, "last-pb-context-exited", true);
}
PrivateBrowsingTrackingProtectionWhitelist.prototype = {
classID: Components.ID("{a319b616-c45d-4037-8d86-01c592b5a9af}"),
QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrivateBrowsingTrackingProtectionWhitelist,
Ci.nsIObserver,
Ci.nsISupportsWeakReference,
Ci.nsISupports]),
_xpcom_factory: XPCOMUtils.generateSingletonFactory(PrivateBrowsingTrackingProtectionWhitelist),
/**
* Add the provided URI to the list of allowed tracking sites.
*
* @param uri nsIURI
* The URI to add to the list.
*/
addToAllowList(uri) {
if (this._allowlist.indexOf(uri.spec) === -1) {
this._allowlist.push(uri.spec);
}
},
/**
* Remove the provided URI from the list of allowed tracking sites.
*
* @param uri nsIURI
* The URI to add to the list.
*/
removeFromAllowList(uri) {
let index = this._allowlist.indexOf(uri.spec);
if (index !== -1) {
this._allowlist.splice(index, 1);
}
},
/**
* Check if the provided URI exists in the list of allowed tracking sites.
*
* @param uri nsIURI
* The URI to add to the list.
*/
existsInAllowList(uri) {
return this._allowlist.indexOf(uri.spec) !== -1;
},
observe: function (subject, topic, data) {
if (topic == "last-pb-context-exited") {
this._allowlist = [];
}
}
};
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PrivateBrowsingTrackingProtectionWhitelist]);

View File

@ -7,6 +7,7 @@
TEST_DIRS += ['tests']
XPIDL_SOURCES += [
'nsIPrivateBrowsingTrackingProtectionWhitelist.idl',
'nsIUrlClassifierDBService.idl',
'nsIUrlClassifierHashCompleter.idl',
'nsIUrlClassifierPrefixSet.idl',
@ -42,6 +43,7 @@ SOURCES += [
EXTRA_COMPONENTS += [
'nsURLClassifier.manifest',
'nsUrlClassifierHashCompleter.js',
'PrivateBrowsingTrackingProtectionWhitelist.js',
]
# Same as JS components that are run through the pre-processor.

View File

@ -0,0 +1,46 @@
/* 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/. */
#include "nsISupports.idl"
interface nsIURI;
/**
* The Private Browsing Tracking Protection service checks a URI against an
* in-memory list of tracking sites.
*/
[scriptable, uuid(c77ddfac-6cd6-43a9-84e8-91682a1a7b18)]
interface nsIPrivateBrowsingTrackingProtectionWhitelist : nsISupports
{
/**
* Add a URI to the list of allowed tracking sites in Private Browsing mode
* (essentially a tracking whitelist). This operation will cause the URI to
* be registered if it does not currently exist. If it already exists, then
* the operation is essentially a no-op.
*
* @param uri the uri to add to the list
*/
void addToAllowList(in nsIURI uri);
/**
* Remove a URI from the list of allowed tracking sites in Private Browsing
* mode (the tracking whitelist). If the URI is not already in the list,
* then the operation is essentially a no-op.
*
* @param uri the uri to remove from the list
*/
void removeFromAllowList(in nsIURI uri);
/**
* Check if a URI exists in the list of allowed tracking sites in Private
* Browsing mode (the tracking whitelist).
*
* @param uri the uri to look for in the list
*/
bool existsInAllowList(in nsIURI uri);
};
%{ C++
#define NS_PBTRACKINGPROTECTIONWHITELIST_CONTRACTID "@mozilla.org/url-classifier/pbm-tp-whitelist;1"
%}

View File

@ -4,3 +4,5 @@ component {ca168834-cc00-48f9-b83c-fd018e58cae3} nsUrlClassifierListManager.js
contract @mozilla.org/url-classifier/listmanager;1 {ca168834-cc00-48f9-b83c-fd018e58cae3}
component {9111de73-9322-4bfc-8b65-2b727f3e6ec8} nsUrlClassifierHashCompleter.js
contract @mozilla.org/url-classifier/hashcompleter;1 {9111de73-9322-4bfc-8b65-2b727f3e6ec8}
component {a319b616-c45d-4037-8d86-01c592b5a9af} PrivateBrowsingTrackingProtectionWhitelist.js
contract @mozilla.org/url-classifier/pbm-tp-whitelist;1 {a319b616-c45d-4037-8d86-01c592b5a9af}

View File

@ -68,15 +68,15 @@ module.exports = function makeDebugger({ findDebuggees, shouldAddNewGlobalAsDebu
dbg.uncaughtExceptionHook = reportDebuggerHookException;
dbg.onNewGlobalObject = global => {
dbg.onNewGlobalObject = function(global) {
if (shouldAddNewGlobalAsDebuggee(global)) {
safeAddDebuggee(dbg, global);
safeAddDebuggee(this, global);
}
};
dbg.addDebuggees = () => {
for (let global of findDebuggees(dbg)) {
safeAddDebuggee(dbg, global);
dbg.addDebuggees = function() {
for (let global of findDebuggees(this)) {
safeAddDebuggee(this, global);
}
};

View File

@ -51,6 +51,18 @@ this.PrivateBrowsingUtils = {
.QueryInterface(Ci.nsILoadContext);
},
addToTrackingAllowlist(aURI) {
let pbmtpWhitelist = Cc["@mozilla.org/url-classifier/pbm-tp-whitelist;1"]
.getService(Ci.nsIPrivateBrowsingTrackingProtectionWhitelist);
pbmtpWhitelist.addToAllowList(aURI);
},
removeFromTrackingAllowlist(aURI) {
let pbmtpWhitelist = Cc["@mozilla.org/url-classifier/pbm-tp-whitelist;1"]
.getService(Ci.nsIPrivateBrowsingTrackingProtectionWhitelist);
pbmtpWhitelist.removeFromAllowList(aURI);
},
get permanentPrivateBrowsing() {
try {
return gTemporaryAutoStartMode ||

View File

@ -119,8 +119,8 @@ Installer.prototype = {
if (install.linkedInstalls) {
install.linkedInstalls.forEach(function(aInstall) {
aInstall.addListener(this);
// App disabled items are not compatible and so fail to install
if (aInstall.addon.appDisabled)
// Corrupt or incompatible items fail to install
if (aInstall.state == AddonManager.STATE_DOWNLOAD_FAILED || aInstall.addon.appDisabled)
failed.push(aInstall);
else
installs.push(aInstall);
@ -132,7 +132,7 @@ Installer.prototype = {
break;
default:
logger.warn("Download of " + install.sourceURI.spec + " in unexpected state " +
install.state);
install.state);
}
}

View File

@ -5126,7 +5126,7 @@ AddonInstall.prototype = {
}
let self = this;
this.loadManifest().then(() => {
this.loadManifest(this.file).then(() => {
XPIDatabase.getVisibleAddonForID(self.addon.id, function initLocalInstall_getVisibleAddon(aAddon) {
self.existingAddon = aAddon;
if (aAddon)
@ -5162,6 +5162,10 @@ AddonInstall.prototype = {
logger.warn("Invalid XPI", message);
this.state = AddonManager.STATE_DOWNLOAD_FAILED;
this.error = error;
AddonManagerPrivate.callInstallListeners("onNewInstall",
self.listeners,
self.wrapper);
aCallback(this);
});
},
@ -5337,6 +5341,45 @@ AddonInstall.prototype = {
this.addon.releaseNotesURI = this.releaseNotesURI.spec;
},
/**
* Fills out linkedInstalls with AddonInstall instances for the other files
* in a multi-package XPI.
*
* @param aFiles
* An array of { entryName, file } for each remaining file from the
* multi-package XPI.
*/
_createLinkedInstalls: Task.async(function* AI_createLinkedInstalls(aFiles) {
if (aFiles.length == 0)
return;
// Create new AddonInstall instances for every remaining file
if (!this.linkedInstalls)
this.linkedInstalls = [];
for (let { entryName, file } of aFiles) {
logger.debug("Creating linked install from " + entryName);
let install = yield new Promise(resolve => AddonInstall.createInstall(resolve, file));
// Make the new install own its temporary file
install.ownsTempFile = true;
this.linkedInstalls.push(install);
// If one of the internal XPIs was multipackage then move its linked
// installs to the outer install
if (install.linkedInstalls) {
this.linkedInstalls.push(...install.linkedInstalls);
install.linkedInstalls = null;
}
install.sourceURI = this.sourceURI;
install.releaseNotesURI = this.releaseNotesURI;
if (install.state != AddonManager.STATE_DOWNLOAD_FAILED)
install.updateAddonURIs();
}
}),
/**
* Loads add-on manifests from a multi-package XPI file. Each of the
* XPI and JAR files contained in the XPI will be extracted. Any that
@ -5347,97 +5390,58 @@ AddonInstall.prototype = {
* @param aZipReader
* An open nsIZipReader for the multi-package XPI's files. This will
* be closed before this method returns.
* @param aCallback
* A function to call when all of the add-on manifests have been
* loaded. Because this loadMultipackageManifests is an internal API
* we don't exception-wrap this callback
*/
_loadMultipackageManifests: Task.async(function* AI_loadMultipackageManifests(aZipReader) {
let files = [];
let entries = aZipReader.findEntries("(*.[Xx][Pp][Ii]|*.[Jj][Aa][Rr])");
while (entries.hasMore()) {
let entryName = entries.getNext();
var target = getTemporaryFile();
let file = getTemporaryFile();
try {
aZipReader.extract(entryName, target);
files.push(target);
aZipReader.extract(entryName, file);
files.push({ entryName, file });
}
catch (e) {
logger.warn("Failed to extract " + entryName + " from multi-package " +
"XPI", e);
target.remove(false);
file.remove(false);
}
}
aZipReader.close();
if (files.length == 0) {
throw new Error("Multi-package XPI does not contain any packages " +
"to install");
return Promise.reject([AddonManager.ERROR_CORRUPT_FILE,
"Multi-package XPI does not contain any packages to install"]);
}
let addon = null;
// Find the first file that has a valid install manifest and use it for
// Find the first file that is a valid install and use it for
// the add-on that this AddonInstall instance will install.
while (files.length > 0) {
for (let { entryName, file } of files) {
this.removeTemporaryFile();
this.file = files.shift();
this.ownsTempFile = true;
try {
addon = yield loadManifestFromZipFile(this.file);
break;
yield this.loadManifest(file);
logger.debug("Base multi-package XPI install came from " + entryName);
this.file = file;
this.ownsTempFile = true;
yield this._createLinkedInstalls(files.filter(f => f.file != file));
return;
}
catch (e) {
logger.warn(this.file.leafName + " cannot be installed from multi-package " +
"XPI", e);
// _createLinkedInstalls will log errors when it tries to process this
// file
}
}
if (!addon) {
// No valid add-on was found
return;
}
// No valid add-on was found, delete all the temporary files
for (let { file } of files)
file.remove(true);
this.addon = addon;
this.updateAddonURIs();
this.addon._install = this;
this.name = this.addon.selectedLocale.name;
this.type = this.addon.type;
this.version = this.addon.version;
// Setting the iconURL to something inside the XPI locks the XPI and
// makes it impossible to delete on Windows.
//let newIcon = createWrapper(this.addon).iconURL;
//if (newIcon)
// this.iconURL = newIcon;
// Create new AddonInstall instances for every remaining file
if (files.length > 0) {
this.linkedInstalls = [];
let self = this;
for (let file of files) {
let install = yield new Promise(resolve => AddonInstall.createInstall(resolve, file));
// Ignore bad add-ons (createInstall will have logged the error)
if (install.state == AddonManager.STATE_DOWNLOAD_FAILED) {
// Manually remove the temporary file
file.remove(true);
}
else {
// Make the new install own its temporary file
install.ownsTempFile = true;
self.linkedInstalls.push(install)
install.sourceURI = self.sourceURI;
install.releaseNotesURI = self.releaseNotesURI;
install.updateAddonURIs();
}
}
}
return Promise.reject([AddonManager.ERROR_CORRUPT_FILE,
"Multi-package XPI does not contain any valid packages to install"]);
}),
/**
@ -5449,11 +5453,11 @@ AddonInstall.prototype = {
* @throws if the add-on does not contain a valid install manifest or the
* XPI is incorrectly signed
*/
loadManifest: Task.async(function* AI_loadManifest() {
loadManifest: Task.async(function* AI_loadManifest(file) {
let zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].
createInstance(Ci.nsIZipReader);
try {
zipreader.open(this.file);
zipreader.open(file);
}
catch (e) {
zipreader.close();
@ -5771,7 +5775,7 @@ AddonInstall.prototype = {
}
let self = this;
this.loadManifest().then(() => {
this.loadManifest(this.file).then(() => {
if (self.addon.isCompatible) {
self.downloadCompleted();
}
@ -5860,9 +5864,10 @@ AddonInstall.prototype = {
self.install();
if (self.linkedInstalls) {
self.linkedInstalls.forEach(function(aInstall) {
aInstall.install();
});
for (let install of self.linkedInstalls) {
if (install.state == AddonManager.STATE_DOWNLOADED)
install.install();
}
}
}
});

View File

@ -0,0 +1 @@
This isn't a valid zip file.

View File

@ -0,0 +1 @@
This isn't a valid zip file.

View File

@ -0,0 +1,10 @@
<?xml version="1.0"?>
<!-- A multi-package XPI -->
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:type>32</em:type>
</Description>
</RDF>

View File

@ -0,0 +1,10 @@
<?xml version="1.0"?>
<!-- A multi-package XPI -->
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
<Description about="urn:mozilla:install-manifest">
<em:type>32</em:type>
</Description>
</RDF>

View File

@ -132,6 +132,32 @@
<featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
</gfxBlacklistEntry>
<gfxBlacklistEntry>
<os>All</os>
<vendor>0xabcd</vendor>
<versionRange minVersion="42.0" maxVersion="13.0b2"/>
<devices>
<device>0x2783</device>
<device>0x1234</device>
<device>0x2782</device>
</devices>
<feature> WEBRTC_HW_ACCELERATION_ENCODE </feature>
<featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
</gfxBlacklistEntry>
<gfxBlacklistEntry>
<os>All</os>
<vendor>0xabcd</vendor>
<versionRange minVersion="42.0" maxVersion="13.0b2"/>
<devices>
<device>0x2783</device>
<device>0x1234</device>
<device>0x2782</device>
</devices>
<feature> WEBRTC_HW_ACCELERATION_DECODE </feature>
<featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
</gfxBlacklistEntry>
<gfxBlacklistEntry>
<os>All</os>
<vendor>0xabcd</vendor>

View File

@ -1108,9 +1108,13 @@ const AddonListener = {
const InstallListener = {
onNewInstall: function(install) {
if (install.state != AddonManager.STATE_DOWNLOADED &&
install.state != AddonManager.STATE_DOWNLOAD_FAILED &&
install.state != AddonManager.STATE_AVAILABLE)
do_throw("Bad install state " + install.state);
do_check_eq(install.error, 0);
if (install.state != AddonManager.STATE_DOWNLOAD_FAILED)
do_check_eq(install.error, 0);
else
do_check_neq(install.error, 0);
do_check_eq("onNewInstall", getExpectedInstall());
return check_test_completed(arguments);
},

View File

@ -107,6 +107,12 @@ function run_test() {
status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBRTC_HW_ACCELERATION);
do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBRTC_HW_ACCELERATION_ENCODE);
do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBRTC_HW_ACCELERATION_DECODE);
do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_11_LAYERS);
do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);

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