This commit is contained in:
Ryan VanderMeulen 2015-06-19 16:33:00 -04:00
Родитель 9f4176e010 196c201118
Коммит 38908bda34
111 изменённых файлов: 3303 добавлений и 340 удалений

Просмотреть файл

@ -1442,9 +1442,10 @@ pref("devtools.debugger.pretty-print-enabled", true);
pref("devtools.debugger.auto-pretty-print", false);
pref("devtools.debugger.auto-black-box", true);
pref("devtools.debugger.tracer", false);
pref("devtools.debugger.workers", false);
// The default Debugger UI settings
pref("devtools.debugger.ui.panes-sources-width", 200);
pref("devtools.debugger.ui.panes-workers-and-sources-width", 200);
pref("devtools.debugger.ui.panes-instruments-width", 300);
pref("devtools.debugger.ui.panes-visible-on-startup", false);
pref("devtools.debugger.ui.variables-sorting-enabled", true);
@ -1496,12 +1497,14 @@ pref("devtools.netmonitor.filters", "[\"all\"]");
// The default Network monitor HAR export setting
pref("devtools.netmonitor.har.defaultLogDir", "");
pref("devtools.netmonitor.har.defaultFileName", "archive");
pref("devtools.netmonitor.har.defaultFileName", "Archive %y-%m-%d %H-%M-%S");
pref("devtools.netmonitor.har.jsonp", false);
pref("devtools.netmonitor.har.jsonpCallback", "");
pref("devtools.netmonitor.har.includeResponseBodies", true);
pref("devtools.netmonitor.har.compress", false);
pref("devtools.netmonitor.har.forceExport", false);
pref("devtools.netmonitor.har.pageLoadedTimeout", 1500);
pref("devtools.netmonitor.har.enableAutoExportToFile", false);
// Enable the Tilt inspector
pref("devtools.tilt.enabled", true);

Просмотреть файл

@ -3371,6 +3371,16 @@
return;
}
// Instrumentation to figure out bug 1166351 - if the binding
// on the browser we're switching to has gone away, try to find out
// why. We should remove this and the checkBrowserBindingAlive
// method once bug 1166351 has been closed.
if (this.tabbrowser.AppConstants.E10S_TESTING_ONLY &&
!this.checkBrowserBindingAlive(tab)) {
Cu.reportError("Please report the above errors in bug 1166351.");
return;
}
this.logState("requestTab " + this.tinfo(tab));
this.startTabSwitch();
@ -3466,6 +3476,40 @@
return this._shouldLog;
},
// Instrumentation for bug 1166351
checkBrowserBindingAlive: function(tab) {
let err = Cu.reportError;
if (!tab.linkedBrowser) {
err("Attempting to switch to tab that has no linkedBrowser.");
return false;
}
let b = tab.linkedBrowser;
if (!b.isRemoteBrowser) {
// non-remote browsers are not the problem.
return true;
}
if (!b._alive) {
// The browser binding has been removed. Dump a bunch of
// diagnostic information to the browser console.
err("The tabbrowser-remote-browser binding has been removed " +
"from the tab being switched to.");
err("MozBinding is currently: " +
window.getComputedStyle(b).MozBinding);
if (!b.parentNode) {
err("Browser was removed from the DOM.");
} else {
err("Parent is: " + b.parentNode.outerHTML);
}
return false;
}
return true;
},
tinfo: function(tab) {
if (tab) {
return tab._tPos + "(" + tab.linkedBrowser.currentURI.spec + ")";
@ -6117,6 +6161,8 @@
<binding id="tabbrowser-remote-browser"
extends="chrome://global/content/bindings/remote-browser.xml#remote-browser">
<implementation>
<!-- This will go away if the binding has been removed for some reason. -->
<field name="_alive">true</field>
<!-- throws exception for unknown schemes -->
<method name="loadURIWithFlags">
<parameter name="aURI"/>

Просмотреть файл

@ -6,6 +6,8 @@ support-files =
[browser_referrer_middle_click.js]
[browser_referrer_open_link_in_private.js]
skip-if = os == 'linux' # Bug 1145199
[browser_referrer_open_link_in_tab.js]
[browser_referrer_open_link_in_window.js]
skip-if = os == 'linux' # Bug 1145199
[browser_referrer_simple_click.js]

Просмотреть файл

@ -123,6 +123,18 @@ static RedirEntry kRedirMap[] = {
nsIAboutModule::URI_MUST_LOAD_IN_CHILD |
nsIAboutModule::MAKE_UNLINKABLE |
nsIAboutModule::HIDE_FROM_ABOUTABOUT },
{
"pocket-saved", "chrome://browser/content/pocket/panels/saved.html",
nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
nsIAboutModule::ALLOW_SCRIPT |
nsIAboutModule::HIDE_FROM_ABOUTABOUT |
nsIAboutModule::MAKE_UNLINKABLE },
{
"pocket-signup", "chrome://browser/content/pocket/panels/signup.html",
nsIAboutModule::URI_SAFE_FOR_UNTRUSTED_CONTENT |
nsIAboutModule::ALLOW_SCRIPT |
nsIAboutModule::HIDE_FROM_ABOUTABOUT |
nsIAboutModule::MAKE_UNLINKABLE },
};
static const int kRedirTotal = ArrayLength(kRedirMap);

Просмотреть файл

@ -115,6 +115,8 @@ static const mozilla::Module::ContractIDEntry kBrowserContracts[] = {
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "looppanel", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "loopconversation", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "reader", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "pocket-saved", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
{ NS_ABOUT_MODULE_CONTRACTID_PREFIX "pocket-signup", &kNS_BROWSER_ABOUT_REDIRECTOR_CID },
#if defined(XP_WIN)
{ NS_IEHISTORYENUMERATOR_CONTRACTID, &kNS_WINIEHISTORYENUMERATOR_CID },
#elif defined(XP_MACOSX)

Просмотреть файл

@ -648,6 +648,9 @@ loop.roomViews = (function(mozL10n) {
* room state and other flags.
*
* @return {Boolean} True if remote video should be rended.
*
* XXX Refactor shouldRenderRemoteVideo & shouldRenderLoading into one fn
* that returns an enum
*/
shouldRenderRemoteVideo: function() {
switch(this.state.roomState) {
@ -684,6 +687,31 @@ loop.roomViews = (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
*/
_shouldRenderLocalLoading: function () {
return this.state.roomState === ROOM_STATES.MEDIA_WAIT &&
!this.state.localSrcVideoObject;
},
/**
* 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
*/
_shouldRenderRemoteLoading: function() {
return this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS &&
!this.state.remoteSrcVideoObject &&
!this.state.mediaConnected;
},
render: function() {
if (this.state.roomName) {
this.setTitle(this.state.roomName);
@ -742,6 +770,7 @@ loop.roomViews = (function(mozL10n) {
React.createElement("div", {className: "video_inner remote focus-stream"},
React.createElement(sharedViews.MediaView, {displayAvatar: !this.shouldRenderRemoteVideo(),
posterUrl: this.props.remotePosterUrl,
isLoading: this._shouldRenderRemoteLoading(),
mediaType: "remote",
srcVideoObject: this.state.remoteSrcVideoObject})
)
@ -749,6 +778,7 @@ loop.roomViews = (function(mozL10n) {
React.createElement("div", {className: localStreamClasses},
React.createElement(sharedViews.MediaView, {displayAvatar: this.state.videoMuted,
posterUrl: this.props.localPosterUrl,
isLoading: this._shouldRenderLocalLoading(),
mediaType: "local",
srcVideoObject: this.state.localSrcVideoObject})
)

Просмотреть файл

@ -648,6 +648,9 @@ loop.roomViews = (function(mozL10n) {
* room state and other flags.
*
* @return {Boolean} True if remote video should be rended.
*
* XXX Refactor shouldRenderRemoteVideo & shouldRenderLoading into one fn
* that returns an enum
*/
shouldRenderRemoteVideo: function() {
switch(this.state.roomState) {
@ -684,6 +687,31 @@ loop.roomViews = (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
*/
_shouldRenderLocalLoading: function () {
return this.state.roomState === ROOM_STATES.MEDIA_WAIT &&
!this.state.localSrcVideoObject;
},
/**
* 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
*/
_shouldRenderRemoteLoading: function() {
return this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS &&
!this.state.remoteSrcVideoObject &&
!this.state.mediaConnected;
},
render: function() {
if (this.state.roomName) {
this.setTitle(this.state.roomName);
@ -742,6 +770,7 @@ loop.roomViews = (function(mozL10n) {
<div className="video_inner remote focus-stream">
<sharedViews.MediaView displayAvatar={!this.shouldRenderRemoteVideo()}
posterUrl={this.props.remotePosterUrl}
isLoading={this._shouldRenderRemoteLoading()}
mediaType="remote"
srcVideoObject={this.state.remoteSrcVideoObject} />
</div>
@ -749,6 +778,7 @@ loop.roomViews = (function(mozL10n) {
<div className={localStreamClasses}>
<sharedViews.MediaView displayAvatar={this.state.videoMuted}
posterUrl={this.props.localPosterUrl}
isLoading={this._shouldRenderLocalLoading()}
mediaType="local"
srcVideoObject={this.state.localSrcVideoObject} />
</div>

Просмотреть файл

@ -553,6 +553,39 @@
width: 100%;
}
/*
* Used to center the loading spinner
*/
.focus-stream, .remote {
position: relative;
}
.loading-stream {
/* vertical and horizontal center */
position: absolute;
top: 50%;
left: 50%;
margin-left: -50px;
margin-top: -50px;
width: 100px;
height: 100px;
/* place the animation */
background-image: url("../img/spinner.svg");
background-position: center;
background-repeat: no-repeat;
background-size: 40%;
/* 12 is the number of lines in the spinner image */
animation: rotate-spinner 1s steps(12, end) infinite;
}
@keyframes rotate-spinner {
to {
transform: rotate(360deg);
}
}
.conversation .local .avatar {
position: absolute;
z-index: 1;
@ -1596,3 +1629,11 @@ html[dir="rtl"] .standalone .room-conversation-wrapper .room-inner-info-area {
width: 100%;
height: 100%;
}
/* Make sure the loading spinner always gets the same background */
.loading-background {
background: black;
position: absolute;
width: 100%;
height: 100%;
}

Просмотреть файл

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="171px" height="171px" viewBox="0 0 171 171" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:sketch="http://www.bohemiancoding.com/sketch/ns">
<!-- Generator: Sketch 3.3.2 (12043) - http://www.bohemiancoding.com/sketch -->
<title>FX_Hello-glyph-spinner-16x16</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="spinner" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" sketch:type="MSPage">
<g id="hello-spinner" sketch:type="MSLayerGroup">
<g id="Group" sketch:type="MSShapeGroup">
<path d="M94.05,34.24275 C94.05,38.966625 90.223875,42.79275 85.5,42.79275 L85.5,42.79275 C80.776125,42.79275 76.95,38.966625 76.95,34.24275 L76.95,8.55 C76.95,3.826125 80.776125,0 85.5,0 L85.5,0 C90.223875,0 94.05,3.826125 94.05,8.55 L94.05,34.24275 L94.05,34.24275 Z" id="Shape" fill="#A4A4A4"></path>
<path d="M94.05,162.45 C94.05,167.173875 90.223875,171 85.5,171 L85.5,171 C80.776125,171 76.95,167.173875 76.95,162.45 L76.95,136.75725 C76.95,132.033375 80.776125,128.20725 85.5,128.20725 L85.5,128.20725 C90.223875,128.20725 94.05,132.033375 94.05,136.75725 L94.05,162.45 L94.05,162.45 Z" id="Shape" fill="#FFFFFF"></path>
<path d="M34.24275,76.95 C38.966625,76.95 42.79275,80.776125 42.79275,85.5 L42.79275,85.5 C42.79275,90.223875 38.966625,94.05 34.24275,94.05 L8.55,94.05 C3.826125,94.05 0,90.223875 0,85.5 L0,85.5 C0,80.776125 3.826125,76.95 8.55,76.95 L34.24275,76.95 L34.24275,76.95 Z" id="Shape" fill="#616161"></path>
<path d="M162.45,76.95 C167.173875,76.95 171,80.776125 171,85.5 L171,85.5 C171,90.223875 167.173875,94.05 162.45,94.05 L136.75725,94.05 C132.033375,94.05 128.20725,90.223875 128.20725,85.5 L128.20725,85.5 C128.20725,80.776125 132.033375,76.95 136.75725,76.95 L162.45,76.95 L162.45,76.95 Z" id="Shape" fill="#C5C5C5"></path>
<path d="M36.8398125,103.743562 C40.933125,101.381625 46.1593125,102.781688 48.52125,106.864312 L48.52125,106.864312 C50.8831875,110.957625 49.483125,116.183813 45.4005,118.54575 L23.149125,131.402812 C19.0558125,133.76475 13.829625,132.364688 11.4676875,128.271375 L11.4676875,128.271375 C9.10575,124.18875 10.5058125,118.951875 14.5884375,116.600625 L36.8398125,103.743562 L36.8398125,103.743562 Z" id="Shape" fill="#4F4F4F"></path>
<path d="M147.861562,39.607875 C151.944187,37.2459375 157.181062,38.646 159.543,42.728625 L159.543,42.728625 C161.904938,46.81125 160.504875,52.048125 156.42225,54.399375 L134.170875,67.2564375 C130.077563,69.618375 124.851375,68.2183125 122.489438,64.1356875 L122.489438,64.1356875 C120.1275,60.0530625 121.527563,54.8161875 125.610188,52.4649375 L147.861562,39.607875 L147.861562,39.607875 Z" id="Shape" fill="#BABABA"></path>
<path d="M103.732875,134.160188 C101.370938,130.077563 102.771,124.840688 106.853625,122.489438 L106.853625,122.489438 C110.93625,120.1275 116.173125,121.527563 118.535063,125.620875 L131.392125,147.87225 C133.754063,151.954875 132.354,157.19175 128.271375,159.543 L128.271375,159.543 C124.18875,161.904938 118.951875,160.504875 116.589938,156.42225 L103.732875,134.160188 L103.732875,134.160188 Z" id="Shape" fill="#E0E0E0"></path>
<path d="M39.607875,23.149125 C37.2459375,19.0665 38.646,13.829625 42.728625,11.4676875 L42.728625,11.4676875 C46.81125,9.10575 52.048125,10.5058125 54.4100625,14.599125 L67.267125,36.8505 C69.618375,40.933125 68.2183125,46.1593125 64.1356875,48.52125 L64.1356875,48.52125 C60.042375,50.8831875 54.8161875,49.483125 52.45425,45.4005 L39.607875,23.149125 L39.607875,23.149125 Z" id="Shape" fill="#989898"></path>
<path d="M52.5076875,125.652938 C54.8589375,121.559625 60.085125,120.159563 64.1784375,122.510813 L64.1784375,122.510813 C68.27175,124.862063 69.6718125,130.08825 67.3205625,134.181563 L54.4955625,156.454313 C52.1443125,160.547625 46.9074375,161.947688 42.8248125,159.596438 L42.8248125,159.596438 C38.7315,157.245187 37.3314375,152.019 39.6826875,147.925688 L52.5076875,125.652938 L52.5076875,125.652938 Z" id="Shape" fill="#3E3E3E"></path>
<path d="M116.504437,14.556375 C118.866375,10.4630625 124.081875,9.063 128.175187,11.41425 L128.175187,11.41425 C132.2685,13.7655 133.668563,19.002375 131.317313,23.085 L118.492313,45.35775 C116.130375,49.4510625 110.904188,50.851125 106.821563,48.499875 L106.821563,48.499875 C102.72825,46.148625 101.328188,40.9224375 103.679438,36.829125 L116.504437,14.556375 L116.504437,14.556375 Z" id="Shape" fill="#B0B0B0"></path>
<path d="M125.64225,118.503 C121.548937,116.141062 120.148875,110.914875 122.500125,106.83225 L122.500125,106.83225 C124.851375,102.738937 130.077562,101.338875 134.170875,103.690125 L156.432937,116.515125 C160.52625,118.877062 161.926312,124.10325 159.575063,128.185875 L159.575063,128.185875 C157.223812,132.279187 151.997625,133.67925 147.904313,131.328 L125.64225,118.503 L125.64225,118.503 Z" id="Shape" fill="#CECECE"></path>
<path d="M14.556375,54.4955625 C10.4630625,52.1443125 9.063,46.918125 11.41425,42.8248125 L11.41425,42.8248125 C13.7761875,38.7315 19.002375,37.3314375 23.085,39.6826875 L45.35775,52.5076875 C49.4510625,54.869625 50.851125,60.0958125 48.499875,64.1784375 L48.499875,64.1784375 C46.1379375,68.2824375 40.91175,69.6825 36.8184375,67.33125 L14.556375,54.4955625 L14.556375,54.4955625 Z" id="Shape" fill="#7C7C7C"></path>
</g>
</g>
</g>
</svg>

После

Ширина:  |  Высота:  |  Размер: 5.5 KiB

Просмотреть файл

@ -290,8 +290,7 @@ loop.shared.actions = (function() {
/**
* Used to notify that a shared screen is being received (or not).
*
* XXX this is going to need to be split into two actions so when
* can display a spinner when the screen share is pending (bug 1171933)
* XXX this should be split into multiple actions to make the code clearer.
*/
ReceivingScreenShare: Action.define("receivingScreenShare", {
receiving: Boolean

Просмотреть файл

@ -612,8 +612,7 @@ loop.store.ActiveRoomStore = (function() {
/**
* Used to note the current state of receiving screenshare data.
*
* XXX this is going to need to be split into two actions so when
* can display a spinner when the screen share is pending (bug 1171933)
* XXX this should be split into multiple actions to make the code clearer.
*/
receivingScreenShare: function(actionData) {
if (!actionData.receiving &&

Просмотреть файл

@ -512,16 +512,9 @@ loop.OTSdkDriver = (function() {
*/
_handleRemoteScreenShareCreated: function(stream) {
// Let the stores know first so they can update the display.
// XXX We do want to do this - we want them to start re-arranging the
// display so that we can a) indicate connecting, b) be ready for
// when we get the stream. However, we're currently limited by the fact
// the view calculations require the remote (aka screen share) element to
// be present and laid out. Hence, we need to drop this for the time being,
// and let the client know via _onScreenShareSubscribeCompleted.
// Bug 1171933 is going to look at fixing this.
// this.dispatcher.dispatch(new sharedActions.ReceivingScreenShare({
// receiving: true
// }));
this.dispatcher.dispatch(new sharedActions.ReceivingScreenShare({
receiving: true
}));
// There's no audio for screen shares so we don't need to worry about mute.
this._mockScreenShareEl = document.createElement("div");

Просмотреть файл

@ -698,6 +698,21 @@ loop.shared.views = (function(_, l10n) {
}
});
/**
* Renders a loading spinner for when video content is not yet available.
*/
var LoadingView = React.createClass({displayName: "LoadingView",
mixins: [React.addons.PureRenderMixin],
render: function() {
return (
React.createElement("div", {className: "loading-background"},
React.createElement("div", {className: "loading-stream"})
)
);
}
});
/**
* Renders a url that's part of context on the display.
*
@ -778,6 +793,7 @@ loop.shared.views = (function(_, l10n) {
React.createElement("a", {className: "context-url",
onClick: this.handleLinkClick,
href: this.props.allowClick ? this.props.url : null,
rel: "noreferrer",
target: "_blank"}, hostname)
)
)
@ -797,6 +813,7 @@ loop.shared.views = (function(_, l10n) {
PropTypes: {
displayAvatar: React.PropTypes.bool.isRequired,
isLoading: React.PropTypes.bool.isRequired,
posterUrl: React.PropTypes.string,
// Expecting "local" or "remote".
mediaType: React.PropTypes.string.isRequired,
@ -850,7 +867,8 @@ loop.shared.views = (function(_, l10n) {
// src is for Chrome.
attrName = "src";
} else {
console.error("Error attaching stream to element - no supported attribute found");
console.error("Error attaching stream to element - no supported" +
"attribute found");
return;
}
@ -862,6 +880,10 @@ loop.shared.views = (function(_, l10n) {
},
render: function() {
if (this.props.isLoading) {
return React.createElement(LoadingView, null);
}
if (this.props.displayAvatar) {
return React.createElement(AvatarView, null);
}
@ -902,6 +924,7 @@ loop.shared.views = (function(_, l10n) {
ConversationToolbar: ConversationToolbar,
MediaControlButton: MediaControlButton,
MediaView: MediaView,
LoadingView: LoadingView,
ScreenShareControlButton: ScreenShareControlButton,
NotificationListView: NotificationListView
};

Просмотреть файл

@ -698,6 +698,21 @@ loop.shared.views = (function(_, l10n) {
}
});
/**
* Renders a loading spinner for when video content is not yet available.
*/
var LoadingView = React.createClass({
mixins: [React.addons.PureRenderMixin],
render: function() {
return (
<div className="loading-background">
<div className="loading-stream"/>
</div>
);
}
});
/**
* Renders a url that's part of context on the display.
*
@ -778,6 +793,7 @@ loop.shared.views = (function(_, l10n) {
<a className="context-url"
onClick={this.handleLinkClick}
href={this.props.allowClick ? this.props.url : null}
rel="noreferrer"
target="_blank">{hostname}</a>
</span>
</div>
@ -797,6 +813,7 @@ loop.shared.views = (function(_, l10n) {
PropTypes: {
displayAvatar: React.PropTypes.bool.isRequired,
isLoading: React.PropTypes.bool.isRequired,
posterUrl: React.PropTypes.string,
// Expecting "local" or "remote".
mediaType: React.PropTypes.string.isRequired,
@ -850,7 +867,8 @@ loop.shared.views = (function(_, l10n) {
// src is for Chrome.
attrName = "src";
} else {
console.error("Error attaching stream to element - no supported attribute found");
console.error("Error attaching stream to element - no supported" +
"attribute found");
return;
}
@ -862,6 +880,10 @@ loop.shared.views = (function(_, l10n) {
},
render: function() {
if (this.props.isLoading) {
return <LoadingView />;
}
if (this.props.displayAvatar) {
return <AvatarView />;
}
@ -902,6 +924,7 @@ loop.shared.views = (function(_, l10n) {
ConversationToolbar: ConversationToolbar,
MediaControlButton: MediaControlButton,
MediaView: MediaView,
LoadingView: LoadingView,
ScreenShareControlButton: ScreenShareControlButton,
NotificationListView: NotificationListView
};

Просмотреть файл

@ -35,6 +35,9 @@ browser.jar:
content/browser/loop/shared/img/sad.png (content/shared/img/sad.png)
content/browser/loop/shared/img/icon_32.png (content/shared/img/icon_32.png)
content/browser/loop/shared/img/icon_64.png (content/shared/img/icon_64.png)
content/browser/loop/shared/img/spinner.svg (content/shared/img/spinner.svg)
# XXX could get rid of the png spinner usages and replace them with the svg
# one?
content/browser/loop/shared/img/spinner.png (content/shared/img/spinner.png)
content/browser/loop/shared/img/spinner@2x.png (content/shared/img/spinner@2x.png)
content/browser/loop/shared/img/audio-inverse-14x14.png (content/shared/img/audio-inverse-14x14.png)

Просмотреть файл

@ -7,6 +7,7 @@
<meta charset="utf-8">
<meta name="locales" content="en-US" />
<meta name="default_locale" content="en-US" />
<meta name="referrer" content="origin" />
<link rel="shortcut icon" href="favicon.ico">

Просмотреть файл

@ -176,6 +176,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
React.createElement("h1", null, mozL10n.get("clientShortname2")),
React.createElement("a", {href: loop.config.generalSupportUrl,
onClick: this.recordClick,
rel: "noreferrer",
target: "_blank"},
React.createElement("i", {className: "icon icon-help"})
)
@ -196,12 +197,12 @@ loop.standaloneRoomViews = (function(mozL10n) {
return mozL10n.get("legal_text_and_links", {
"clientShortname": mozL10n.get("clientShortname2"),
"terms_of_use_url": React.renderToStaticMarkup(
React.createElement("a", {href: loop.config.legalWebsiteUrl, target: "_blank"},
React.createElement("a", {href: loop.config.legalWebsiteUrl, rel: "noreferrer", target: "_blank"},
mozL10n.get("terms_of_use_link_text")
)
),
"privacy_notice_url": React.renderToStaticMarkup(
React.createElement("a", {href: loop.config.privacyWebsiteUrl, target: "_blank"},
React.createElement("a", {href: loop.config.privacyWebsiteUrl, rel: "noreferrer", target: "_blank"},
mozL10n.get("privacy_notice_link_text")
)
)
@ -328,6 +329,9 @@ loop.standaloneRoomViews = (function(mozL10n) {
* room state and other flags.
*
* @return {Boolean} True if remote video should be rended.
*
* XXX Refactor shouldRenderRemoteVideo & shouldRenderLoading to remove
* overlapping cases.
*/
shouldRenderRemoteVideo: function() {
switch(this.state.roomState) {
@ -366,6 +370,43 @@ loop.standaloneRoomViews = (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
*/
_shouldRenderLocalLoading: function () {
return this.state.roomState === ROOM_STATES.MEDIA_WAIT &&
!this.state.localSrcVideoObject;
},
/**
* 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
*/
_shouldRenderRemoteLoading: function() {
return this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS &&
!this.state.remoteSrcVideoObject &&
!this.state.mediaConnected;
},
/**
* Should we render a visual cue to the user (e.g. a spinner) that a remote
* screen-share is on its way from the other user?
*
* @returns {boolean}
* @private
*/
_shouldRenderScreenShareLoading: function() {
return this.state.receivingScreenShare &&
!this.state.screenShareVideoObject;
},
render: function() {
var displayScreenShare = this.state.receivingScreenShare ||
this.props.screenSharePosterUrl;
@ -405,12 +446,14 @@ loop.standaloneRoomViews = (function(mozL10n) {
React.createElement("div", {className: remoteStreamClasses},
React.createElement(sharedViews.MediaView, {displayAvatar: !this.shouldRenderRemoteVideo(),
posterUrl: this.props.remotePosterUrl,
isLoading: this._shouldRenderRemoteLoading(),
mediaType: "remote",
srcVideoObject: this.state.remoteSrcVideoObject})
),
React.createElement("div", {className: screenShareStreamClasses},
React.createElement(sharedViews.MediaView, {displayAvatar: false,
posterUrl: this.props.screenSharePosterUrl,
isLoading: this._shouldRenderScreenShareLoading(),
mediaType: "screen-share",
srcVideoObject: this.state.screenShareVideoObject})
),
@ -421,6 +464,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
React.createElement("div", {className: "local"},
React.createElement(sharedViews.MediaView, {displayAvatar: this.state.videoMuted,
posterUrl: this.props.localPosterUrl,
isLoading: this._shouldRenderLocalLoading(),
mediaType: "local",
srcVideoObject: this.state.localSrcVideoObject})
)

Просмотреть файл

@ -176,6 +176,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
<h1>{mozL10n.get("clientShortname2")}</h1>
<a href={loop.config.generalSupportUrl}
onClick={this.recordClick}
rel="noreferrer"
target="_blank">
<i className="icon icon-help"></i>
</a>
@ -196,12 +197,12 @@ loop.standaloneRoomViews = (function(mozL10n) {
return mozL10n.get("legal_text_and_links", {
"clientShortname": mozL10n.get("clientShortname2"),
"terms_of_use_url": React.renderToStaticMarkup(
<a href={loop.config.legalWebsiteUrl} target="_blank">
<a href={loop.config.legalWebsiteUrl} rel="noreferrer" target="_blank">
{mozL10n.get("terms_of_use_link_text")}
</a>
),
"privacy_notice_url": React.renderToStaticMarkup(
<a href={loop.config.privacyWebsiteUrl} target="_blank">
<a href={loop.config.privacyWebsiteUrl} rel="noreferrer" target="_blank">
{mozL10n.get("privacy_notice_link_text")}
</a>
)
@ -328,6 +329,9 @@ loop.standaloneRoomViews = (function(mozL10n) {
* room state and other flags.
*
* @return {Boolean} True if remote video should be rended.
*
* XXX Refactor shouldRenderRemoteVideo & shouldRenderLoading to remove
* overlapping cases.
*/
shouldRenderRemoteVideo: function() {
switch(this.state.roomState) {
@ -366,6 +370,43 @@ loop.standaloneRoomViews = (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
*/
_shouldRenderLocalLoading: function () {
return this.state.roomState === ROOM_STATES.MEDIA_WAIT &&
!this.state.localSrcVideoObject;
},
/**
* 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
*/
_shouldRenderRemoteLoading: function() {
return this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS &&
!this.state.remoteSrcVideoObject &&
!this.state.mediaConnected;
},
/**
* Should we render a visual cue to the user (e.g. a spinner) that a remote
* screen-share is on its way from the other user?
*
* @returns {boolean}
* @private
*/
_shouldRenderScreenShareLoading: function() {
return this.state.receivingScreenShare &&
!this.state.screenShareVideoObject;
},
render: function() {
var displayScreenShare = this.state.receivingScreenShare ||
this.props.screenSharePosterUrl;
@ -405,12 +446,14 @@ loop.standaloneRoomViews = (function(mozL10n) {
<div className={remoteStreamClasses}>
<sharedViews.MediaView displayAvatar={!this.shouldRenderRemoteVideo()}
posterUrl={this.props.remotePosterUrl}
isLoading={this._shouldRenderRemoteLoading()}
mediaType="remote"
srcVideoObject={this.state.remoteSrcVideoObject} />
</div>
<div className={screenShareStreamClasses}>
<sharedViews.MediaView displayAvatar={false}
posterUrl={this.props.screenSharePosterUrl}
isLoading={this._shouldRenderScreenShareLoading()}
mediaType="screen-share"
srcVideoObject={this.state.screenShareVideoObject} />
</div>
@ -421,6 +464,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
<div className="local">
<sharedViews.MediaView displayAvatar={this.state.videoMuted}
posterUrl={this.props.localPosterUrl}
isLoading={this._shouldRenderLocalLoading()}
mediaType="local"
srcVideoObject={this.state.localSrcVideoObject} />
</div>

Просмотреть файл

@ -528,6 +528,58 @@ describe("loop.roomViews", function () {
loop.shared.views.FeedbackView);
});
it("should display loading spinner when localSrcVideoObject is null",
function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.MEDIA_WAIT,
localSrcVideoObject: null
});
view = mountTestComponent();
expect(view.getDOMNode().querySelector(".local .loading-stream"))
.not.eql(null);
});
it("should not display a loading spinner when local stream available",
function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.MEDIA_WAIT,
localSrcVideoObject: { fake: "video" }
});
view = mountTestComponent();
expect(view.getDOMNode().querySelector(".local .loading-stream"))
.eql(null);
});
it("should display loading spinner when remote stream is not available",
function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
remoteSrcVideoObject: null
});
view = mountTestComponent();
expect(view.getDOMNode().querySelector(".remote .loading-stream"))
.not.eql(null);
});
it("should not display a loading spinner when remote stream available",
function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
remoteSrcVideoObject: { fake: "video" }
});
view = mountTestComponent();
expect(view.getDOMNode().querySelector(".remote .loading-stream"))
.eql(null);
});
it("should display an avatar for remote video when the room has participants but video is not enabled",
function() {
activeRoomStore.setStoreState({

Просмотреть файл

@ -992,9 +992,7 @@ describe("loop.OTSdkDriver", function () {
new sharedActions.ReceivingScreenShare({receiving: true}));
});
// XXX See bug 1171933 and the comment in
// OtSdkDriver#_handleRemoteScreenShareCreated
it.skip("should dispatch a ReceivingScreenShare action for screen" +
it("should dispatch a ReceivingScreenShare action for screen" +
" sharing streams", function() {
fakeStream.videoType = "screen";

Просмотреть файл

@ -240,6 +240,43 @@ describe("loop.standaloneRoomViews", function() {
});
});
describe("screenShare", function() {
it("should show a loading screen if receivingScreenShare is true " +
"but no screenShareVideoObject is present", function() {
view.setState({
"receivingScreenShare": true,
"screenShareVideoObject": null
});
expect(view.getDOMNode().querySelector(".screen .loading-stream"))
.not.eql(null);
});
it("should not show loading screen if receivingScreenShare is false " +
"and screenShareVideoObject is null", function() {
view.setState({
"receivingScreenShare": false,
"screenShareVideoObject": null
});
expect(view.getDOMNode().querySelector(".screen .loading-stream"))
.eql(null);
});
it("should not show a loading screen if screenShareVideoObject is set",
function() {
var videoElement = document.createElement("video");
view.setState({
"receivingScreenShare": true,
"screenShareVideoObject": videoElement
});
expect(view.getDOMNode().querySelector(".screen .loading-stream"))
.eql(null);
});
});
describe("Participants", function() {
var videoElement;
@ -266,6 +303,50 @@ describe("loop.standaloneRoomViews", function() {
expect(view.getDOMNode().querySelector(".local .avatar")).eql(null);
});
it("should render local loading screen when no srcVideoObject",
function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.MEDIA_WAIT,
remoteSrcVideoObject: null
});
expect(view.getDOMNode().querySelector(".local .loading-stream"))
.not.eql(null);
});
it("should not render local loading screen when srcVideoObject is set",
function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.MEDIA_WAIT,
localSrcVideoObject: videoElement
});
expect(view.getDOMNode().querySelector(".local .loading-stream"))
.eql(null);
});
it("should not render remote loading screen when srcVideoObject is set",
function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
remoteSrcVideoObject: videoElement
});
expect(view.getDOMNode().querySelector(".remote .loading-stream"))
.eql(null);
});
it("should render remote video when the room HAS_PARTICIPANTS and" +
" remoteVideoEnabled is true", function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
remoteSrcVideoObject: videoElement,
remoteVideoEnabled: true
});
expect(view.getDOMNode().querySelector(".remote video")).not.eql(null);
});
it("should render remote video when the room HAS_PARTICIPANTS and" +
" remoteVideoEnabled is true", function() {
activeRoomStore.setStoreState({
@ -333,6 +414,18 @@ describe("loop.standaloneRoomViews", function() {
expect(view.getDOMNode().querySelector(".remote .avatar")).not.eql(null);
});
it("should render a remote avatar when the room HAS_PARTICIPANTS, " +
"remoteSrcVideoObject is false, mediaConnected is true", function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
remoteSrcVideoObject: false,
remoteVideoEnabled: false,
mediaConnected: true
});
expect(view.getDOMNode().querySelector(".remote .avatar")).not.eql(null);
});
});
describe("Leave button", function() {

Просмотреть файл

@ -9,6 +9,10 @@ body {
overflow: visible;
}
.overflow-hidden {
overflow: hidden;
}
.showcase {
width: 100%;
margin: 0 auto;

Просмотреть файл

@ -179,6 +179,12 @@
remoteVideoEnabled: false
});
var loadingRemoteVideoRoomStore = makeActiveRoomStore({
mediaConnected: false,
roomState: ROOM_STATES.HAS_PARTICIPANTS,
remoteSrcVideoObject: false
});
var readyRoomStore = makeActiveRoomStore({
mediaConnected: false,
roomState: ROOM_STATES.READY
@ -204,6 +210,25 @@
receivingScreenShare: true
});
var loadingRemoteLoadingScreenStore = makeActiveRoomStore({
mediaConnected: false,
roomState: ROOM_STATES.HAS_PARTICIPANTS,
remoteSrcVideoObject: false
});
var loadingScreenSharingRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS
});
/* Set up the stores for pending screen sharing */
loadingScreenSharingRoomStore.receivingScreenShare({
receiving: true,
srcVideoObject: false
});
loadingRemoteLoadingScreenStore.receivingScreenShare({
receiving: true,
srcVideoObject: false
});
var fullActiveRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.FULL
});
@ -217,10 +242,26 @@
roomUsed: true
});
var roomStore = new loop.store.RoomStore(dispatcher, {
var invitationRoomStore = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop
});
var roomStore = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop,
activeRoomStore: makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS
})
});
var desktopRoomStoreLoading = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop,
activeRoomStore: makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
mediaConnected: false,
remoteSrcVideoObject: false
})
});
var desktopLocalFaceMuteActiveRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
videoMuted: true
@ -783,7 +824,7 @@
summary: "Desktop room conversation (invitation)"},
React.createElement("div", {className: "fx-embedded"},
React.createElement(DesktopRoomConversationView, {
roomStore: roomStore,
roomStore: invitationRoomStore,
dispatcher: dispatcher,
mozLoop: navigator.mozLoop,
localPosterUrl: "sample-img/video-screen-local.png",
@ -791,6 +832,21 @@
)
),
React.createElement(FramedExample, {width: 298, height: 254,
summary: "Desktop room conversation (loading)"},
/* Hide scrollbars here. Rotating loading div overflows and causes
scrollbars to appear */
React.createElement("div", {className: "fx-embedded overflow-hidden"},
React.createElement(DesktopRoomConversationView, {
roomStore: desktopRoomStoreLoading,
dispatcher: dispatcher,
mozLoop: navigator.mozLoop,
localPosterUrl: "sample-img/video-screen-local.png",
remotePosterUrl: "sample-img/video-screen-remote.png",
roomState: ROOM_STATES.HAS_PARTICIPANTS})
)
),
React.createElement(FramedExample, {width: 298, height: 254,
summary: "Desktop room conversation"},
React.createElement("div", {className: "fx-embedded"},
@ -853,6 +909,19 @@
)
),
React.createElement(FramedExample, {width: 644, height: 483, dashed: true,
summary: "Standalone room conversation (loading remote)",
cssClass: "standalone",
onContentsRendered: loadingRemoteVideoRoomStore.forcedUpdate},
React.createElement("div", {className: "standalone"},
React.createElement(StandaloneRoomView, {
dispatcher: dispatcher,
activeRoomStore: loadingRemoteVideoRoomStore,
localPosterUrl: "sample-img/video-screen-local.png",
isFirefox: true})
)
),
React.createElement(FramedExample, {width: 644, height: 483, dashed: true,
cssClass: "standalone",
onContentsRendered: updatingActiveRoomStore.forcedUpdate,
@ -896,6 +965,44 @@
)
),
React.createElement(FramedExample, {width: 800, height: 660, dashed: true,
cssClass: "standalone",
onContentsRendered: loadingRemoteLoadingScreenStore.forcedUpdate,
summary: "Standalone room convo (has-participants, loading screen share, loading remote video, 800x660)"},
/* Hide scrollbars here. Rotating loading div overflows and causes
scrollbars to appear */
React.createElement("div", {className: "standalone overflow-hidden"},
React.createElement(StandaloneRoomView, {
dispatcher: dispatcher,
activeRoomStore: loadingRemoteLoadingScreenStore,
roomState: ROOM_STATES.HAS_PARTICIPANTS,
isFirefox: true,
localPosterUrl: "sample-img/video-screen-local.png",
remotePosterUrl: "sample-img/video-screen-remote.png",
screenSharePosterUrl: "sample-img/video-screen-baz.png"}
)
)
),
React.createElement(FramedExample, {width: 800, height: 660, dashed: true,
cssClass: "standalone",
onContentsRendered: loadingScreenSharingRoomStore.forcedUpdate,
summary: "Standalone room convo (has-participants, loading screen share, 800x660)"},
/* Hide scrollbars here. Rotating loading div overflows and causes
scrollbars to appear */
React.createElement("div", {className: "standalone overflow-hidden"},
React.createElement(StandaloneRoomView, {
dispatcher: dispatcher,
activeRoomStore: loadingScreenSharingRoomStore,
roomState: ROOM_STATES.HAS_PARTICIPANTS,
isFirefox: true,
localPosterUrl: "sample-img/video-screen-local.png",
remotePosterUrl: "sample-img/video-screen-remote.png",
screenSharePosterUrl: "sample-img/video-screen-baz.png"}
)
)
),
React.createElement(FramedExample, {width: 800, height: 660, dashed: true,
cssClass: "standalone",
onContentsRendered: updatingSharingRoomStore.forcedUpdate,

Просмотреть файл

@ -179,6 +179,12 @@
remoteVideoEnabled: false
});
var loadingRemoteVideoRoomStore = makeActiveRoomStore({
mediaConnected: false,
roomState: ROOM_STATES.HAS_PARTICIPANTS,
remoteSrcVideoObject: false
});
var readyRoomStore = makeActiveRoomStore({
mediaConnected: false,
roomState: ROOM_STATES.READY
@ -204,6 +210,25 @@
receivingScreenShare: true
});
var loadingRemoteLoadingScreenStore = makeActiveRoomStore({
mediaConnected: false,
roomState: ROOM_STATES.HAS_PARTICIPANTS,
remoteSrcVideoObject: false
});
var loadingScreenSharingRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS
});
/* Set up the stores for pending screen sharing */
loadingScreenSharingRoomStore.receivingScreenShare({
receiving: true,
srcVideoObject: false
});
loadingRemoteLoadingScreenStore.receivingScreenShare({
receiving: true,
srcVideoObject: false
});
var fullActiveRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.FULL
});
@ -217,10 +242,26 @@
roomUsed: true
});
var roomStore = new loop.store.RoomStore(dispatcher, {
var invitationRoomStore = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop
});
var roomStore = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop,
activeRoomStore: makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS
})
});
var desktopRoomStoreLoading = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop,
activeRoomStore: makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
mediaConnected: false,
remoteSrcVideoObject: false
})
});
var desktopLocalFaceMuteActiveRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
videoMuted: true
@ -783,7 +824,7 @@
summary="Desktop room conversation (invitation)">
<div className="fx-embedded">
<DesktopRoomConversationView
roomStore={roomStore}
roomStore={invitationRoomStore}
dispatcher={dispatcher}
mozLoop={navigator.mozLoop}
localPosterUrl="sample-img/video-screen-local.png"
@ -791,6 +832,21 @@
</div>
</FramedExample>
<FramedExample width={298} height={254}
summary="Desktop room conversation (loading)">
{/* Hide scrollbars here. Rotating loading div overflows and causes
scrollbars to appear */}
<div className="fx-embedded overflow-hidden">
<DesktopRoomConversationView
roomStore={desktopRoomStoreLoading}
dispatcher={dispatcher}
mozLoop={navigator.mozLoop}
localPosterUrl="sample-img/video-screen-local.png"
remotePosterUrl="sample-img/video-screen-remote.png"
roomState={ROOM_STATES.HAS_PARTICIPANTS} />
</div>
</FramedExample>
<FramedExample width={298} height={254}
summary="Desktop room conversation">
<div className="fx-embedded">
@ -853,6 +909,19 @@
</div>
</FramedExample>
<FramedExample width={644} height={483} dashed={true}
summary="Standalone room conversation (loading remote)"
cssClass="standalone"
onContentsRendered={loadingRemoteVideoRoomStore.forcedUpdate}>
<div className="standalone">
<StandaloneRoomView
dispatcher={dispatcher}
activeRoomStore={loadingRemoteVideoRoomStore}
localPosterUrl="sample-img/video-screen-local.png"
isFirefox={true} />
</div>
</FramedExample>
<FramedExample width={644} height={483} dashed={true}
cssClass="standalone"
onContentsRendered={updatingActiveRoomStore.forcedUpdate}
@ -896,6 +965,44 @@
</div>
</FramedExample>
<FramedExample width={800} height={660} dashed={true}
cssClass="standalone"
onContentsRendered={loadingRemoteLoadingScreenStore.forcedUpdate}
summary="Standalone room convo (has-participants, loading screen share, loading remote video, 800x660)">
{/* Hide scrollbars here. Rotating loading div overflows and causes
scrollbars to appear */}
<div className="standalone overflow-hidden">
<StandaloneRoomView
dispatcher={dispatcher}
activeRoomStore={loadingRemoteLoadingScreenStore}
roomState={ROOM_STATES.HAS_PARTICIPANTS}
isFirefox={true}
localPosterUrl="sample-img/video-screen-local.png"
remotePosterUrl="sample-img/video-screen-remote.png"
screenSharePosterUrl="sample-img/video-screen-baz.png"
/>
</div>
</FramedExample>
<FramedExample width={800} height={660} dashed={true}
cssClass="standalone"
onContentsRendered={loadingScreenSharingRoomStore.forcedUpdate}
summary="Standalone room convo (has-participants, loading screen share, 800x660)">
{/* Hide scrollbars here. Rotating loading div overflows and causes
scrollbars to appear */}
<div className="standalone overflow-hidden">
<StandaloneRoomView
dispatcher={dispatcher}
activeRoomStore={loadingScreenSharingRoomStore}
roomState={ROOM_STATES.HAS_PARTICIPANTS}
isFirefox={true}
localPosterUrl="sample-img/video-screen-local.png"
remotePosterUrl="sample-img/video-screen-remote.png"
screenSharePosterUrl="sample-img/video-screen-baz.png"
/>
</div>
</FramedExample>
<FramedExample width={800} height={660} dashed={true}
cssClass="standalone"
onContentsRendered={updatingSharingRoomStore.forcedUpdate}

Просмотреть файл

@ -321,7 +321,7 @@ var pktUI = (function() {
{
variant = pktApi.getSignupAB();
}
var panelId = showPanel("chrome://browser/content/pocket/panels/signup.html?pockethost=" + Services.prefs.getCharPref("browser.pocket.site") + "&fxasignedin=" + fxasignedin + "&variant=" + variant + '&inoverflowmenu=' + inOverflowMenu + "&locale=" + getUILocale(), {
var panelId = showPanel("about:pocket-signup?pockethost=" + Services.prefs.getCharPref("browser.pocket.site") + "&fxasignedin=" + fxasignedin + "&variant=" + variant + '&inoverflowmenu=' + inOverflowMenu + "&locale=" + getUILocale(), {
onShow: function() {
},
onHide: panelDidHide,
@ -349,7 +349,7 @@ var pktUI = (function() {
startheight = overflowMenuHeight;
}
var panelId = showPanel("chrome://browser/content/pocket/panels/saved.html?pockethost=" + Services.prefs.getCharPref("browser.pocket.site") + "&premiumStatus=" + (pktApi.isPremiumUser() ? '1' : '0') + '&inoverflowmenu='+inOverflowMenu + "&locale=" + getUILocale(), {
var panelId = showPanel("about:pocket-saved?pockethost=" + Services.prefs.getCharPref("browser.pocket.site") + "&premiumStatus=" + (pktApi.isPremiumUser() ? '1' : '0') + '&inoverflowmenu='+inOverflowMenu + "&locale=" + getUILocale(), {
onShow: function() {
var saveLinkMessageId = 'saveLink';

Просмотреть файл

@ -1,6 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<base href="chrome://browser/content/pocket/panels/">
<title>Pocket: Page Saved</title>
<link rel="stylesheet" href="css/normalize.css">
<link rel="stylesheet" href="css/firasans.css">

Просмотреть файл

@ -1,6 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<base href="chrome://browser/content/pocket/panels/">
<title>Pocket: Sign Up</title>
<link rel="stylesheet" href="css/normalize.css">
<link rel="stylesheet" href="css/firasans.css">

Просмотреть файл

@ -182,6 +182,9 @@ let DebuggerController = {
this.SourceScripts.disconnect();
this.StackFrames.disconnect();
this.ThreadState.disconnect();
if (this._target.isTabActor) {
this.Workers.disconnect();
}
this.Tracer.disconnect();
this.disconnect();
@ -319,6 +322,7 @@ let DebuggerController = {
return;
}
this.activeThread = aThreadClient;
this.Workers.connect();
this.ThreadState.connect();
this.StackFrames.connect();
this.SourceScripts.connect();
@ -446,6 +450,75 @@ let DebuggerController = {
activeThread: null
};
function Workers() {
this._workerClients = new Map();
this._onWorkerListChanged = this._onWorkerListChanged.bind(this);
this._onWorkerFreeze = this._onWorkerFreeze.bind(this);
this._onWorkerThaw = this._onWorkerThaw.bind(this);
}
Workers.prototype = {
get _tabClient() {
return DebuggerController._target.activeTab;
},
connect: function () {
if (!Prefs.workersEnabled) {
return;
}
this._updateWorkerList();
this._tabClient.addListener("workerListChanged", this._onWorkerListChanged);
},
disconnect: function () {
this._tabClient.removeListener("workerListChanged", this._onWorkerListChanged);
},
_updateWorkerList: function () {
this._tabClient.listWorkers((response) => {
let workerActors = new Set();
for (let worker of response.workers) {
workerActors.add(worker.actor);
}
for (let [workerActor, workerClient] of this._workerClients) {
if (!workerActors.has(workerActor)) {
workerClient.removeListener("freeze", this._onWorkerFreeze);
workerClient.removeListener("thaw", this._onWorkerThaw);
this._workerClients.delete(workerActor);
DebuggerView.Workers.removeWorker(workerActor);
}
}
for (let actor of workerActors) {
let workerActor = actor
if (!this._workerClients.has(workerActor)) {
this._tabClient.attachWorker(workerActor, (response, workerClient) => {
workerClient.addListener("freeze", this._onWorkerFreeze);
workerClient.addListener("thaw", this._onWorkerThaw);
this._workerClients.set(workerActor, workerClient);
DebuggerView.Workers.addWorker(workerActor, workerClient.url);
});
}
}
});
},
_onWorkerListChanged: function () {
this._updateWorkerList();
},
_onWorkerFreeze: function (type, packet) {
DebuggerView.Workers.removeWorker(packet.from);
},
_onWorkerThaw: function (type, packet) {
let workerClient = this._workerClients.get(packet.from);
DebuggerView.Workers.addWorker(packet.from, workerClient.url);
}
};
/**
* ThreadState keeps the UI up to date with the state of the
* thread (paused/attached/etc.).
@ -2424,7 +2497,7 @@ let L10N = new ViewHelpers.L10N(DBG_STRINGS_URI);
* Shortcuts for accessing various debugger preferences.
*/
let Prefs = new ViewHelpers.Prefs("devtools", {
sourcesWidth: ["Int", "debugger.ui.panes-sources-width"],
workersAndSourcesWidth: ["Int", "debugger.ui.panes-workers-and-sources-width"],
instrumentsWidth: ["Int", "debugger.ui.panes-instruments-width"],
panesVisibleOnStartup: ["Bool", "debugger.ui.panes-visible-on-startup"],
variablesSortingEnabled: ["Bool", "debugger.ui.variables-sorting-enabled"],
@ -2436,6 +2509,7 @@ let Prefs = new ViewHelpers.Prefs("devtools", {
prettyPrintEnabled: ["Bool", "debugger.pretty-print-enabled"],
autoPrettyPrint: ["Bool", "debugger.auto-pretty-print"],
tracerEnabled: ["Bool", "debugger.tracer"],
workersEnabled: ["Bool", "debugger.workers"],
editorTabSize: ["Int", "editor.tabsize"],
autoBlackBox: ["Bool", "debugger.auto-black-box"]
});
@ -2450,6 +2524,7 @@ EventEmitter.decorate(this);
*/
DebuggerController.initialize();
DebuggerController.Parser = new Parser();
DebuggerController.Workers = new Workers();
DebuggerController.ThreadState = new ThreadState();
DebuggerController.StackFrames = new StackFrames();
DebuggerController.SourceScripts = new SourceScripts();

Просмотреть файл

@ -55,6 +55,7 @@ let DebuggerView = {
this.Filtering.initialize();
this.StackFrames.initialize();
this.StackFramesClassicList.initialize();
this.Workers.initialize();
this.Sources.initialize();
this.VariableBubble.initialize();
this.Tracer.initialize();
@ -108,7 +109,7 @@ let DebuggerView = {
this._body = document.getElementById("body");
this._editorDeck = document.getElementById("editor-deck");
this._sourcesPane = document.getElementById("sources-pane");
this._workersAndSourcesPane = document.getElementById("workers-and-sources-pane");
this._instrumentsPane = document.getElementById("instruments-pane");
this._instrumentsPaneToggleButton = document.getElementById("instruments-pane-toggle");
@ -123,7 +124,7 @@ let DebuggerView = {
this._collapsePaneString = L10N.getStr("collapsePanes");
this._expandPaneString = L10N.getStr("expandPanes");
this._sourcesPane.setAttribute("width", Prefs.sourcesWidth);
this._workersAndSourcesPane.setAttribute("width", Prefs.workersAndSourcesWidth);
this._instrumentsPane.setAttribute("width", Prefs.instrumentsWidth);
this.toggleInstrumentsPane({ visible: Prefs.panesVisibleOnStartup });
@ -140,11 +141,11 @@ let DebuggerView = {
dumpn("Destroying the DebuggerView panes");
if (gHostType != "side") {
Prefs.sourcesWidth = this._sourcesPane.getAttribute("width");
Prefs.workersAndSourcesWidth = this._workersAndSourcesPane.getAttribute("width");
Prefs.instrumentsWidth = this._instrumentsPane.getAttribute("width");
}
this._sourcesPane = null;
this._workersAndSourcesPane = null;
this._instrumentsPane = null;
this._instrumentsPaneToggleButton = null;
},
@ -613,7 +614,7 @@ let DebuggerView = {
// Move the soruces and instruments panes in a different container.
let splitter = document.getElementById("sources-and-instruments-splitter");
vertContainer.insertBefore(this._sourcesPane, splitter);
vertContainer.insertBefore(this._workersAndSourcesPane, splitter);
vertContainer.appendChild(this._instrumentsPane);
// Make sure the vertical layout container's height doesn't repeatedly
@ -632,12 +633,12 @@ let DebuggerView = {
// The sources and instruments pane need to be inserted at their
// previous locations in their normal container.
let splitter = document.getElementById("sources-and-editor-splitter");
normContainer.insertBefore(this._sourcesPane, splitter);
normContainer.insertBefore(this._workersAndSourcesPane, splitter);
normContainer.appendChild(this._instrumentsPane);
// Revert to the preferred sources and instruments widths, because
// they flexed in the vertical layout.
this._sourcesPane.setAttribute("width", Prefs.sourcesWidth);
this._workersAndSourcesPane.setAttribute("width", Prefs.workersAndSourcesWidth);
this._instrumentsPane.setAttribute("width", Prefs.instrumentsWidth);
},
@ -682,7 +683,7 @@ let DebuggerView = {
_loadingText: "",
_body: null,
_editorDeck: null,
_sourcesPane: null,
_workersAndSourcesPane: null,
_instrumentsPane: null,
_instrumentsPaneToggleButton: null,
_collapsePaneString: "",

Просмотреть файл

@ -6,6 +6,7 @@
/* Side pane views */
#workers-pane > tabpanels > tabpanel,
#sources-pane > tabpanels > tabpanel,
#instruments-pane > tabpanels > tabpanel {
-moz-box-orient: vertical;
@ -23,7 +24,7 @@
-moz-box-orient: vertical;
}
#body[layout=vertical] #sources-pane {
#body[layout=vertical] #workers-and-sources-pane {
-moz-box-flex: 1;
}

Просмотреть файл

@ -28,6 +28,7 @@
<script type="text/javascript" src="debugger-controller.js"/>
<script type="text/javascript" src="debugger-view.js"/>
<script type="text/javascript" src="debugger/utils.js"/>
<script type="text/javascript" src="debugger/workers-view.js"/>
<script type="text/javascript" src="debugger/sources-view.js"/>
<script type="text/javascript" src="debugger/variable-bubble-view.js"/>
<script type="text/javascript" src="debugger/tracer-view.js"/>
@ -312,8 +313,24 @@
<vbox id="globalsearch" orient="vertical" hidden="true"/>
<splitter class="devtools-horizontal-splitter" hidden="true"/>
<hbox id="debugger-widgets" flex="1">
<vbox id="workers-and-sources-pane">
<tabbox id="workers-pane"
class="devtools-sidebar-tabs"
flex="0"
hidden="true">
<tabs>
<tab id="workers-tab" label="&debuggerUI.tabs.workers;"/>
</tabs>
<tabpanels flex="1">
<tabpanel>
<vbox id="workers" flex="1"/>
</tabpanel>
</tabpanels>
</tabbox>
<splitter class="devtools-horizontal-splitter"/>
<tabbox id="sources-pane"
class="devtools-sidebar-tabs">
class="devtools-sidebar-tabs"
flex="1">
<tabs>
<tab id="sources-tab" label="&debuggerUI.tabs.sources;"/>
<tab id="callstack-tab" label="&debuggerUI.tabs.callstack;"/>
@ -367,6 +384,7 @@
</tabpanel>
</tabpanels>
</tabbox>
</vbox>
<splitter id="sources-and-editor-splitter"
class="devtools-side-splitter"/>
<deck id="editor-deck" flex="1" class="devtools-main-content">

Просмотреть файл

@ -71,13 +71,13 @@ function testHost(aTab, aPanel, aHostType, aLayoutType) {
"The default host type is present as an attribute on the panel's body.");
if (aLayoutType == "horizontal") {
is(gView._sourcesPane.parentNode.id, "debugger-widgets",
"The sources pane's parent is correct for the horizontal layout.");
is(gView._workersAndSourcesPane.parentNode.id, "debugger-widgets",
"The workers and sources pane's parent is correct for the horizontal layout.");
is(gView._instrumentsPane.parentNode.id, "debugger-widgets",
"The instruments pane's parent is correct for the horizontal layout.");
} else {
is(gView._sourcesPane.parentNode.id, "vertical-layout-panes-container",
"The sources pane's parent is correct for the vertical layout.");
is(gView._workersAndSourcesPane.parentNode.id, "vertical-layout-panes-container",
"The workers and sources pane's parent is correct for the vertical layout.");
is(gView._instrumentsPane.parentNode.id, "vertical-layout-panes-container",
"The instruments pane's parent is correct for the vertical layout.");
}

Просмотреть файл

@ -17,63 +17,63 @@ function test() {
gPanel = aPanel;
gDebugger = gPanel.panelWin;
gPrefs = gDebugger.Prefs;
gSources = gDebugger.document.getElementById("sources-pane");
gSources = gDebugger.document.getElementById("workers-and-sources-pane");
gInstruments = gDebugger.document.getElementById("instruments-pane");
waitForSourceShown(gPanel, ".html").then(performTest);
});
function performTest() {
let preferredSw = Services.prefs.getIntPref("devtools.debugger.ui.panes-sources-width");
let preferredWsw = Services.prefs.getIntPref("devtools.debugger.ui.panes-workers-and-sources-width");
let preferredIw = Services.prefs.getIntPref("devtools.debugger.ui.panes-instruments-width");
let someWidth1, someWidth2;
do {
someWidth1 = parseInt(Math.random() * 200) + 100;
someWidth2 = parseInt(Math.random() * 300) + 100;
} while ((someWidth1 == preferredSw) || (someWidth2 == preferredIw));
} while ((someWidth1 == preferredWsw) || (someWidth2 == preferredIw));
info("Preferred sources width: " + preferredSw);
info("Preferred sources width: " + preferredWsw);
info("Preferred instruments width: " + preferredIw);
info("Generated sources width: " + someWidth1);
info("Generated instruments width: " + someWidth2);
ok(gPrefs.sourcesWidth,
"The debugger preferences should have a saved sourcesWidth value.");
ok(gPrefs.workersAndSourcesWidth,
"The debugger preferences should have a saved workersAndSourcesWidth value.");
ok(gPrefs.instrumentsWidth,
"The debugger preferences should have a saved instrumentsWidth value.");
is(gPrefs.sourcesWidth, preferredSw,
"The debugger preferences should have a correct sourcesWidth value.");
is(gPrefs.workersAndSourcesWidth, preferredWsw,
"The debugger preferences should have a correct workersAndSourcesWidth value.");
is(gPrefs.instrumentsWidth, preferredIw,
"The debugger preferences should have a correct instrumentsWidth value.");
is(gSources.getAttribute("width"), gPrefs.sourcesWidth,
"The sources pane width should be the same as the preferred value.");
is(gSources.getAttribute("width"), gPrefs.workersAndSourcesWidth,
"The workers and sources pane width should be the same as the preferred value.");
is(gInstruments.getAttribute("width"), gPrefs.instrumentsWidth,
"The instruments pane width should be the same as the preferred value.");
gSources.setAttribute("width", someWidth1);
gInstruments.setAttribute("width", someWidth2);
is(gPrefs.sourcesWidth, preferredSw,
"The sources pane width pref should still be the same as the preferred value.");
is(gPrefs.workersAndSourcesWidth, preferredWsw,
"The workers and sources pane width pref should still be the same as the preferred value.");
is(gPrefs.instrumentsWidth, preferredIw,
"The instruments pane width pref should still be the same as the preferred value.");
isnot(gSources.getAttribute("width"), gPrefs.sourcesWidth,
"The sources pane width should not be the preferred value anymore.");
isnot(gSources.getAttribute("width"), gPrefs.workersAndSourcesWidth,
"The workers and sources pane width should not be the preferred value anymore.");
isnot(gInstruments.getAttribute("width"), gPrefs.instrumentsWidth,
"The instruments pane width should not be the preferred value anymore.");
teardown(gPanel).then(() => {
is(gPrefs.sourcesWidth, someWidth1,
"The sources pane width should have been saved by now.");
is(gPrefs.workersAndSourcesWidth, someWidth1,
"The workers and sources pane width should have been saved by now.");
is(gPrefs.instrumentsWidth, someWidth2,
"The instruments pane width should have been saved by now.");
// Cleanup after ourselves!
Services.prefs.setIntPref("devtools.debugger.ui.panes-sources-width", preferredSw);
Services.prefs.setIntPref("devtools.debugger.ui.panes-workers-and-sources-width", preferredWsw);
Services.prefs.setIntPref("devtools.debugger.ui.panes-instruments-width", preferredIw);
finish();

Просмотреть файл

@ -0,0 +1,36 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
function WorkersView() {}
WorkersView.prototype = Heritage.extend(WidgetMethods, {
initialize: function () {
if (!Prefs.workersEnabled) {
return;
}
document.getElementById("workers-pane").removeAttribute("hidden");
this.widget = new SideMenuWidget(document.getElementById("workers"), {
showArrows: true,
});
this.emptyText = L10N.getStr("noWorkersText");
},
addWorker: function (actor, name) {
let element = document.createElement("label");
element.className = "plain dbg-worker-item";
element.setAttribute("value", name);
element.setAttribute("flex", "1");
this.push([element, actor], {});
},
removeWorker: function (actor) {
this.remove(this.getItemByValue(actor));
}
});
DebuggerView.Workers = new WorkersView();

Просмотреть файл

@ -159,6 +159,11 @@
tooltiptext="&options.enableRemote.tooltip;"
data-pref="devtools.debugger.remote-enabled"/>
</hbox>
<hbox class="hidden-labels-box">
<checkbox label="&options.enableWorkers.label;"
tooltiptext="&options.enableWorkers.tooltip;"
data-pref="devtools.debugger.workers"/>
</hbox>
<label class="options-citation-label theme-comment"
>&options.context.triggersPageRefresh;</label>
</vbox>

Просмотреть файл

@ -74,6 +74,9 @@ loader.lazyGetter(this, "oscpu", () => {
loader.lazyGetter(this, "is64Bit", () => {
return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo).is64Bit;
});
loader.lazyGetter(this, "registerHarOverlay", () => {
return require("devtools/netmonitor/har/toolbox-overlay.js").register;
});
// White-list buttons that can be toggled to prevent adding prefs for
// addons that have manually inserted toolbarbuttons into DOM.
@ -375,6 +378,7 @@ Toolbox.prototype = {
this._addKeysToWindow();
this._addReloadKeys();
this._addHostListeners();
this._registerOverlays();
if (this._hostOptions && this._hostOptions.zoom === false) {
this._disableZoomKeys();
} else {
@ -515,6 +519,10 @@ Toolbox.prototype = {
this.doc.addEventListener("focus", this._onFocus, true);
},
_registerOverlays: function() {
registerHarOverlay(this);
},
_saveSplitConsoleHeight: function() {
Services.prefs.setIntPref(SPLITCONSOLE_HEIGHT_PREF,
this.webconsolePanel.height);

Просмотреть файл

@ -68,6 +68,7 @@ browser.jar:
content/browser/devtools/debugger.css (debugger/debugger.css)
content/browser/devtools/debugger-controller.js (debugger/debugger-controller.js)
content/browser/devtools/debugger-view.js (debugger/debugger-view.js)
content/browser/devtools/debugger/workers-view.js (debugger/views/workers-view.js)
content/browser/devtools/debugger/sources-view.js (debugger/views/sources-view.js)
content/browser/devtools/debugger/variable-bubble-view.js (debugger/views/variable-bubble-view.js)
content/browser/devtools/debugger/tracer-view.js (debugger/views/tracer-view.js)

Просмотреть файл

@ -0,0 +1,319 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Cu, Ci, Cc } = require("chrome");
const { Class } = require("sdk/core/heritage");
const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const { defer, resolve } = require("sdk/core/promise");
const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
Cu.import("resource://gre/modules/Task.jsm");
loader.lazyRequireGetter(this, "HarCollector", "devtools/netmonitor/har/har-collector", true);
loader.lazyRequireGetter(this, "HarExporter", "devtools/netmonitor/har/har-exporter", true);
loader.lazyRequireGetter(this, "HarUtils", "devtools/netmonitor/har/har-utils", true);
const prefDomain = "devtools.netmonitor.har.";
// Helper tracer. Should be generic sharable by other modules (bug 1171927)
const trace = {
log: function(...args) {
}
}
/**
* This object is responsible for automated HAR export. It listens
* for Network activity, collects all HTTP data and triggers HAR
* export when the page is loaded.
*
* The user needs to enable the following preference to make the
* auto-export work: devtools.netmonitor.har.enableAutoExportToFile
*
* HAR files are stored within directory that is specified in this
* preference: devtools.netmonitor.har.defaultLogDir
*
* If the default log directory preference isn't set the following
* directory is used by default: <profile>/har/logs
*/
var HarAutomation = Class({
// Initialization
initialize: function(toolbox) {
this.toolbox = toolbox;
let target = toolbox.target;
target.makeRemote().then(() => {
this.startMonitoring(target.client, target.form);
});
},
destroy: function() {
if (this.collector) {
this.collector.stop();
}
if (this.tabWatcher) {
this.tabWatcher.disconnect();
}
},
// Automation
startMonitoring: function(client, tabGrip, callback) {
if (!client) {
return;
}
if (!tabGrip) {
return;
}
this.debuggerClient = client;
this.tabClient = this.toolbox.target.activeTab;
this.webConsoleClient = this.toolbox.target.activeConsole;
let netPrefs = { "NetworkMonitor.saveRequestAndResponseBodies": true };
this.webConsoleClient.setPreferences(netPrefs, () => {
this.tabWatcher = new TabWatcher(this.toolbox, this);
this.tabWatcher.connect();
});
},
pageLoadBegin: function(aResponse) {
this.resetCollector();
},
resetCollector: function() {
if (this.collector) {
this.collector.stop();
}
// A page is about to be loaded, start collecting HTTP
// data from events sent from the backend.
this.collector = new HarCollector({
webConsoleClient: this.webConsoleClient,
debuggerClient: this.debuggerClient
});
this.collector.start();
},
/**
* A page is done loading, export collected data. Note that
* some requests for additional page resources might be pending,
* so export all after all has been properly received from the backend.
*
* This collector still works and collects any consequent HTTP
* traffic (e.g. XHRs) happening after the page is loaded and
* The additional traffic can be exported by executing
* triggerExport on this object.
*/
pageLoadDone: function(aResponse) {
trace.log("HarAutomation.pageLoadDone; ", aResponse);
if (this.collector) {
this.collector.waitForHarLoad().then(collector => {
return this.autoExport();
});
}
},
autoExport: function() {
let autoExport = Services.prefs.getBoolPref(prefDomain +
"enableAutoExportToFile");
if (!autoExport) {
return resolve();
}
// Auto export to file is enabled, so save collected data
// into a file and use all the default options.
let data = {
fileName: Services.prefs.getCharPref(prefDomain + "defaultFileName"),
}
return this.executeExport(data);
},
// Public API
/**
* Export all what is currently collected.
*/
triggerExport: function(data) {
if (!data.fileName) {
data.fileName = Services.prefs.getCharPref(prefDomain +
"defaultFileName");
}
this.executeExport(data);
},
/**
* Clear currently collected data.
*/
clear: function() {
this.resetCollector();
},
// HAR Export
/**
* Execute HAR export. This method fetches all data from the
* Network panel (asynchronously) and saves it into a file.
*/
executeExport: function(data) {
let items = this.collector.getItems();
let form = this.toolbox.target.form;
let title = form.title || form.url;
let options = {
getString: this.getString.bind(this),
view: this,
items: items,
}
options.defaultFileName = data.fileName;
options.compress = data.compress;
options.title = data.title || title;
options.id = data.id;
options.jsonp = data.jsonp;
options.includeResponseBodies = data.includeResponseBodies;
options.jsonpCallback = data.jsonpCallback;
options.forceExport = data.forceExport;
trace.log("HarAutomation.executeExport; " + data.fileName, options);
return HarExporter.fetchHarData(options).then(jsonString => {
// Save the HAR file if the file name is provided.
if (jsonString && options.defaultFileName) {
let file = getDefaultTargetFile(options);
if (file) {
HarUtils.saveToFile(file, jsonString, options.compress);
}
}
return jsonString;
});
},
/**
* Fetches the full text of a string.
*/
getString: function(aStringGrip) {
return this.webConsoleClient.getString(aStringGrip);
},
/**
* Extracts any urlencoded form data sections (e.g. "?foo=bar&baz=42") from a
* POST request.
*
* @param object aHeaders
* The "requestHeaders".
* @param object aUploadHeaders
* The "requestHeadersFromUploadStream".
* @param object aPostData
* The "requestPostData".
* @return array
* A promise that is resolved with the extracted form data.
*/
_getFormDataSections: Task.async(function*(aHeaders, aUploadHeaders, aPostData) {
let formDataSections = [];
let { headers: requestHeaders } = aHeaders;
let { headers: payloadHeaders } = aUploadHeaders;
let allHeaders = [...payloadHeaders, ...requestHeaders];
let contentTypeHeader = allHeaders.find(e => e.name.toLowerCase() == "content-type");
let contentTypeLongString = contentTypeHeader ? contentTypeHeader.value : "";
let contentType = yield this.getString(contentTypeLongString);
if (contentType.includes("x-www-form-urlencoded")) {
let postDataLongString = aPostData.postData.text;
let postData = yield this.getString(postDataLongString);
for (let section of postData.split(/\r\n|\r|\n/)) {
// Before displaying it, make sure this section of the POST data
// isn't a line containing upload stream headers.
if (payloadHeaders.every(header => !section.startsWith(header.name))) {
formDataSections.push(section);
}
}
}
return formDataSections;
}),
});
// Helpers
function TabWatcher(toolbox, listener) {
this.target = toolbox.target;
this.listener = listener;
this.onTabNavigated = this.onTabNavigated.bind(this);
}
TabWatcher.prototype = {
// Connection
connect: function() {
this.target.on("navigate", this.onTabNavigated);
this.target.on("will-navigate", this.onTabNavigated);
},
disconnect: function() {
if (!this.target) {
return;
}
this.target.off("navigate", this.onTabNavigated);
this.target.off("will-navigate", this.onTabNavigated);
},
// Event Handlers
/**
* Called for each location change in the monitored tab.
*
* @param string aType
* Packet type.
* @param object aPacket
* Packet received from the server.
*/
onTabNavigated: function(aType, aPacket) {
switch (aType) {
case "will-navigate": {
this.listener.pageLoadBegin(aPacket);
break;
}
case "navigate": {
this.listener.pageLoadDone(aPacket);
break;
}
}
},
};
// Protocol Helpers
/**
* Returns target file for exported HAR data.
*/
function getDefaultTargetFile(options) {
let path = options.defaultLogDir ||
Services.prefs.getCharPref("devtools.netmonitor.har.defaultLogDir");
let folder = HarUtils.getLocalDirectory(path);
let fileName = HarUtils.getHarFileName(options.defaultFileName,
options.jsonp, options.compress);
folder.append(fileName);
folder.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0666", 8));
return folder;
}
// Exports from this module
exports.HarAutomation = HarAutomation;

Просмотреть файл

@ -7,23 +7,18 @@ const { Cu, Ci, Cc } = require("chrome");
const { defer, all, resolve } = require("sdk/core/promise");
const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
XPCOMUtils.defineLazyGetter(this, "appInfo", function() {
loader.lazyImporter(this, "ViewHelpers", "resource:///modules/devtools/ViewHelpers.jsm");
loader.lazyRequireGetter(this, "NetworkHelper", "devtools/toolkit/webconsole/network-helper");
loader.lazyGetter(this, "appInfo", () => {
return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
});
XPCOMUtils.defineLazyModuleGetter(this, "ViewHelpers",
"resource:///modules/devtools/ViewHelpers.jsm");
XPCOMUtils.defineLazyGetter(this, "L10N", function() {
loader.lazyGetter(this, "L10N", () => {
return new ViewHelpers.L10N("chrome://browser/locale/devtools/har.properties");
});
XPCOMUtils.defineLazyGetter(this, "NetworkHelper", function() {
return devtools.require("devtools/toolkit/webconsole/network-helper");
});
const HAR_VERSION = "1.1";
/**

Просмотреть файл

@ -0,0 +1,455 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Cu, Ci, Cc } = require("chrome");
const { defer, all } = require("sdk/core/promise");
const { setTimeout, clearTimeout } = require("sdk/timers");
const { makeInfallible } = require("devtools/toolkit/DevToolsUtils.js");
const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
// Helper tracer. Should be generic sharable by other modules (bug 1171927)
const trace = {
log: function(...args) {
}
}
/**
* This object is responsible for collecting data related to all
* HTTP requests executed by the page (including inner iframes).
*/
function HarCollector(options) {
this.webConsoleClient = options.webConsoleClient;
this.debuggerClient = options.debuggerClient;
this.onNetworkEvent = this.onNetworkEvent.bind(this);
this.onNetworkEventUpdate = this.onNetworkEventUpdate.bind(this);
this.onRequestHeaders = this.onRequestHeaders.bind(this);
this.onRequestCookies = this.onRequestCookies.bind(this);
this.onRequestPostData = this.onRequestPostData.bind(this);
this.onResponseHeaders = this.onResponseHeaders.bind(this);
this.onResponseCookies = this.onResponseCookies.bind(this);
this.onResponseContent = this.onResponseContent.bind(this);
this.onEventTimings = this.onEventTimings.bind(this);
this.onPageLoadTimeout = this.onPageLoadTimeout.bind(this);
this.clear();
}
HarCollector.prototype = {
// Connection
start: function() {
this.debuggerClient.addListener("networkEvent", this.onNetworkEvent);
this.debuggerClient.addListener("networkEventUpdate", this.onNetworkEventUpdate);
},
stop: function() {
this.debuggerClient.removeListener("networkEvent", this.onNetworkEvent);
this.debuggerClient.removeListener("networkEventUpdate", this.onNetworkEventUpdate);
},
clear: function() {
// Any pending requests events will be ignored (they turn
// into zombies, since not present in the files array).
this.files = new Map();
this.items = [];
this.firstRequestStart = -1;
this.lastRequestStart = -1;
this.requests = [];
},
waitForHarLoad: function() {
// There should be yet another timeout 'devtools.netmonitor.har.pageLoadTimeout'
// that should force export even if page isn't fully loaded.
let deferred = defer();
this.waitForResponses().then(() => {
trace.log("HarCollector.waitForHarLoad; DONE HAR loaded!");
deferred.resolve(this);
});
return deferred.promise;
},
waitForResponses: function() {
trace.log("HarCollector.waitForResponses; " + this.requests.length);
// All requests for additional data must be received to have complete
// HTTP info to generate the result HAR file. So, wait for all current
// promises. Note that new promises (requests) can be generated during the
// process of HTTP data collection.
return waitForAll(this.requests).then(() => {
// All responses are received from the backend now. We yet need to
// wait for a little while to see if a new request appears. If yes,
// lets's start gathering HTTP data again. If no, we can declare
// the page loaded.
// If some new requests appears in the meantime the promise will
// be rejected and we need to wait for responses all over again.
return this.waitForTimeout().then(() => {
// Page loaded!
}, () => {
trace.log("HarCollector.waitForResponses; NEW requests " +
"appeared during page timeout!");
// New requests executed, let's wait again.
return this.waitForResponses();
})
});
},
// Page Loaded Timeout
/**
* The page is loaded when there are no new requests within given period
* of time. The time is set in preferences:
* 'devtools.netmonitor.har.pageLoadedTimeout'
*/
waitForTimeout: function() {
// The auto-export is not done if the timeout is set to zero (or less).
// This is useful in cases where the export is done manually through
// API exposed to the content.
let timeout = Services.prefs.getIntPref(
"devtools.netmonitor.har.pageLoadedTimeout");
trace.log("HarCollector.waitForTimeout; " + timeout);
this.pageLoadDeferred = defer();
if (timeout <= 0) {
this.pageLoadDeferred.resolve();
return this.pageLoadDeferred.promise;
}
this.pageLoadTimeout = setTimeout(this.onPageLoadTimeout, timeout);
return this.pageLoadDeferred.promise;
},
onPageLoadTimeout: function() {
trace.log("HarCollector.onPageLoadTimeout;");
// Ha, page has been loaded. Resolve the final timeout promise.
this.pageLoadDeferred.resolve();
},
resetPageLoadTimeout: function() {
// Remove the current timeout.
if (this.pageLoadTimeout) {
trace.log("HarCollector.resetPageLoadTimeout;");
clearTimeout(this.pageLoadTimeout);
this.pageLoadTimeout = null;
}
// Reject the current page load promise
if (this.pageLoadDeferred) {
this.pageLoadDeferred.reject();
this.pageLoadDeferred = null;
}
},
// Collected Data
getFile: function(actorId) {
return this.files.get(actorId);
},
getItems: function() {
return this.items;
},
// Event Handlers
onNetworkEvent: function(type, packet) {
// Skip events from different console actors.
if (packet.from != this.webConsoleClient.actor) {
return;
}
trace.log("HarCollector.onNetworkEvent; " + type, packet);
let { actor, startedDateTime, method, url, isXHR } = packet.eventActor;
let startTime = Date.parse(startedDateTime);
if (this.firstRequestStart == -1) {
this.firstRequestStart = startTime;
}
if (this.lastRequestEnd < startTime) {
this.lastRequestEnd = startTime;
}
let file = this.getFile(actor);
if (file) {
Cu.reportError("HarCollector.onNetworkEvent; ERROR " +
"existing file conflict!");
return;
}
file = {
startedDeltaMillis: startTime - this.firstRequestStart,
startedMillis: startTime,
method: method,
url: url,
isXHR: isXHR
};
this.files.set(actor, file);
// Mimic the Net panel data structure
this.items.push({
attachment: file
});
},
onNetworkEventUpdate: function(type, packet) {
let actor = packet.from;
// Skip events from unknown actors (not in the list).
// There could also be zombie requests received after the target is closed.
let file = this.getFile(packet.from);
if (!file) {
Cu.reportError("HarCollector.onNetworkEventUpdate; ERROR " +
"Unknown event actor: " + type, packet);
return;
}
trace.log("HarCollector.onNetworkEventUpdate; " +
packet.updateType, packet);
let includeResponseBodies = Services.prefs.getBoolPref(
"devtools.netmonitor.har.includeResponseBodies");
let request;
switch (packet.updateType) {
case "requestHeaders":
request = this.getData(actor, "getRequestHeaders", this.onRequestHeaders);
break;
case "requestCookies":
request = this.getData(actor, "getRequestCookies", this.onRequestCookies);
break;
case "requestPostData":
request = this.getData(actor, "getRequestPostData", this.onRequestPostData);
break;
case "responseHeaders":
request = this.getData(actor, "getResponseHeaders", this.onResponseHeaders);
break;
case "responseCookies":
request = this.getData(actor, "getResponseCookies", this.onResponseCookies);
break;
case "responseStart":
file.httpVersion = packet.response.httpVersion;
file.status = packet.response.status;
file.statusText = packet.response.statusText;
break;
case "responseContent":
file.contentSize = packet.contentSize;
file.mimeType = packet.mimeType;
file.transferredSize = packet.transferredSize;
if (includeResponseBodies) {
request = this.getData(actor, "getResponseContent", this.onResponseContent);
}
break;
case "eventTimings":
request = this.getData(actor, "getEventTimings", this.onEventTimings);
break;
}
if (request) {
this.requests.push(request);
}
this.resetPageLoadTimeout();
},
getData: function(actor, method, callback) {
let deferred = defer();
if (!this.webConsoleClient[method]) {
Cu.reportError("HarCollector.getData; ERROR " +
"Unknown method!");
return;
}
let file = this.getFile(actor);
trace.log("HarCollector.getData; REQUEST " + method +
", " + file.url, file);
this.webConsoleClient[method](actor, response => {
trace.log("HarCollector.getData; RESPONSE " + method +
", " + file.url, response);
callback(response);
deferred.resolve(response);
});
return deferred.promise;
},
/**
* Handles additional information received for a "requestHeaders" packet.
*
* @param object response
* The message received from the server.
*/
onRequestHeaders: function(response) {
let file = this.getFile(response.from);
file.requestHeaders = response;
this.getLongHeaders(response.headers);
},
/**
* Handles additional information received for a "requestCookies" packet.
*
* @param object response
* The message received from the server.
*/
onRequestCookies: function(response) {
let file = this.getFile(response.from);
file.requestCookies = response;
this.getLongHeaders(response.cookies);
},
/**
* Handles additional information received for a "requestPostData" packet.
*
* @param object response
* The message received from the server.
*/
onRequestPostData: function(response) {
trace.log("HarCollector.onRequestPostData;", response);
let file = this.getFile(response.from);
file.requestPostData = response;
// Resolve long string
let text = response.postData.text;
if (typeof text == "object") {
this.getString(text).then(value => {
response.postData.text = value;
})
}
},
/**
* Handles additional information received for a "responseHeaders" packet.
*
* @param object response
* The message received from the server.
*/
onResponseHeaders: function(response) {
let file = this.getFile(response.from);
file.responseHeaders = response;
this.getLongHeaders(response.headers);
},
/**
* Handles additional information received for a "responseCookies" packet.
*
* @param object response
* The message received from the server.
*/
onResponseCookies: function(response) {
let file = this.getFile(response.from);
file.responseCookies = response;
this.getLongHeaders(response.cookies);
},
/**
* Handles additional information received for a "responseContent" packet.
*
* @param object response
* The message received from the server.
*/
onResponseContent: function(response) {
let file = this.getFile(response.from);
file.mimeType = "text/plain";
file.responseContent = response;
// Resolve long string
let text = response.content.text;
if (typeof text == "object") {
this.getString(text).then(value => {
response.content.text = value;
})
}
},
/**
* Handles additional information received for a "eventTimings" packet.
*
* @param object response
* The message received from the server.
*/
onEventTimings: function(response) {
let file = this.getFile(response.from);
file.eventTimings = response;
let totalTime = response.totalTime;
file.totalTime = totalTime;
file.endedMillis = file.startedMillis + totalTime;
},
// Helpers
getLongHeaders: makeInfallible(function(headers) {
for (let header of headers) {
if (typeof header.value == "object") {
this.getString(header.value).then(value => {
header.value = value;
});
}
}
}),
/**
* Fetches the full text of a string.
*
* @param object | string stringGrip
* The long string grip containing the corresponding actor.
* If you pass in a plain string (by accident or because you're lazy),
* then a promise of the same string is simply returned.
* @return object Promise
* A promise that is resolved when the full string contents
* are available, or rejected if something goes wrong.
*/
getString: function(stringGrip) {
let promise = this.webConsoleClient.getString(stringGrip);
this.requests.push(promise);
return promise;
}
};
// Helpers
/**
* Helper function that allows to wait for array of promises. It is
* possible to dynamically add new promises in the provided array.
* The function will wait even for the newly added promises.
* (this isn't possible with the default Promise.all);
*/
function waitForAll(promises) {
// Remove all from the original array and get clone of it.
let clone = promises.splice(0, promises.length);
// Wait for all promises in the given array.
return all(clone).then(() => {
// If there are new promises (in the original array)
// to wait for - chain them!
if (promises.length) {
return waitForAll(promises);
}
});
}
// Exports from this module
exports.HarCollector = HarCollector;

Просмотреть файл

@ -16,6 +16,12 @@ XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
var uid = 1;
// Helper tracer. Should be generic sharable by other modules (bug 1171927)
const trace = {
log: function(...args) {
}
}
/**
* This object represents the main public API designed to access
* Network export logic. Clients, such as the Network panel itself,
@ -77,6 +83,8 @@ const HarExporter = {
return resolve();
}
trace.log("HarExporter.save; " + options.defaultFileName, options);
return this.fetchHarData(options).then(jsonString => {
if (!HarUtils.saveToFile(file, jsonString, options.compress)) {
let msg = "Failed to save HAR file at: " + options.defaultFileName;

Просмотреть файл

@ -4,9 +4,12 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
EXTRA_JS_MODULES.devtools.netmonitor.har += [
'har-automation.js',
'har-builder.js',
'har-collector.js',
'har-exporter.js',
'har-utils.js',
'toolbox-overlay.js',
]
BROWSER_CHROME_MANIFESTS += ['test/browser.ini']

Просмотреть файл

@ -0,0 +1,81 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Cu, Ci } = require("chrome");
const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
loader.lazyRequireGetter(this, "HarAutomation", "devtools/netmonitor/har/har-automation", true);
// Map of all created overlays. There is always one instance of
// an overlay per Toolbox instance (i.e. one per browser tab).
const overlays = new WeakMap();
/**
* This object is responsible for initialization and cleanup for HAR
* export feature. It represents an overlay for the Toolbox
* following the same life time by listening to its events.
*
* HAR APIs are designed for integration with tools (such as Selenium)
* that automates the browser. Primarily, it is for automating web apps
* and getting HAR file for every loaded page.
*/
function ToolboxOverlay(toolbox) {
this.toolbox = toolbox;
this.onInit = this.onInit.bind(this);
this.onDestroy = this.onDestroy.bind(this);
this.toolbox.on("ready", this.onInit);
this.toolbox.on("destroy", this.onDestroy);
}
ToolboxOverlay.prototype = {
/**
* Executed when the toolbox is ready.
*/
onInit: function() {
let autoExport = Services.prefs.getBoolPref(
"devtools.netmonitor.har.enableAutoExportToFile");
if (!autoExport) {
return;
}
this.initAutomation();
},
/**
* Executed when the toolbox is destroyed.
*/
onDestroy: function(eventId, toolbox) {
this.destroyAutomation();
},
// Automation
initAutomation: function() {
this.automation = new HarAutomation(this.toolbox);
},
destroyAutomation: function() {
if (this.automation) {
this.automation.destroy();
}
},
};
// Registration
function register(toolbox) {
if (overlays.has(toolbox)) {
throw Error("There is an existing overlay for the toolbox");
}
// Instantiate an overlay for the toolbox.
let overlay = new ToolboxOverlay(toolbox);
overlays.set(toolbox, overlay);
}
// Exports from this module
exports.register = register;

Просмотреть файл

@ -165,6 +165,7 @@
<!-- LOCALIZATION NOTE (debuggerUI.tabs.*): This is the text that
- appears in the debugger's side pane tabs. -->
<!ENTITY debuggerUI.tabs.workers "Workers">
<!ENTITY debuggerUI.tabs.sources "Sources">
<!ENTITY debuggerUI.tabs.traces "Traces">
<!ENTITY debuggerUI.tabs.callstack "Call Stack">

Просмотреть файл

@ -78,8 +78,12 @@ stepOutTooltip=Step Out (%S)
# when there are no chrome globals available.
noGlobalsText=No globals
# LOCALIZATION NOTE (noSourcesText): The text to display in the sources menu
# when there are no scripts.
# LOCALIZATION NOTE (noWorkersText): The text to display in the workers list
# when there are no workers.
noWorkersText=This page has no workers.
# LOCALIZATION NOTE (noSourcesText): The text to display in the sources list
# when there are no sources.
noSourcesText=This page has no sources.
# LOCALIZATION NOTE (loadingSourcesText): The text to display in the sources menu
@ -320,4 +324,5 @@ resumptionOrderPanelTitle=There are one or more paused debuggers. Please resume
variablesViewOptimizedOut=(optimized away)
variablesViewUninitialized=(uninitialized)
variablesViewMissingArgs=(unavailable)
anonymousSourcesLabel=Anonymous Sources

Просмотреть файл

@ -92,6 +92,12 @@
<!ENTITY options.enableRemote.label3 "Enable remote debugging">
<!ENTITY options.enableRemote.tooltip "Turning this option on will allow the developer tools to debug remote Firefox instance like Firefox OS">
<!-- LOCALIZATION NOTE (options.enableWorkers.label): This is the label for the
- checkbox that toggles worker debugging, i.e. devtools.debugger.workers
- boolean preference in about:config, in the options panel. -->
<!ENTITY options.enableWorkers.label "Enable worker debugging (in development)">
<!ENTITY options.enableWorkers.tooltip "Turning this option on will allow the developer tools to debug workers">
<!-- LOCALIZATION NOTE (options.disableJavaScript.label,
- options.disableJavaScript.tooltip): This is the options panel label and
- tooltip for the checkbox that toggles JavaScript on or off. -->

Просмотреть файл

@ -147,6 +147,10 @@ accessible/xpcom/export: xpcom/xpidl/export
# The widget binding generator code is part of the annotationProcessors.
widget/android/bindings/export: build/annotationProcessors/export
# The roboextender addon includes a classes.dex containing a test Java addon.
# The test addon must be built first.
mobile/android/tests/browser/robocop/roboextender/tools: mobile/android/tests/javaaddons/tools
ifdef ENABLE_CLANG_PLUGIN
$(filter-out build/clang-plugin/%,$(compile_targets)): build/clang-plugin/target build/clang-plugin/tests/target
build/clang-plugin/tests/target: build/clang-plugin/target

Просмотреть файл

@ -791,8 +791,7 @@ pref("gfx.canvas.azure.backends", "skia");
pref("gfx.canvas.azure.accelerated", true);
// See ua-update.json.in for the packaged UA override list
// See Bug 1163759 before setting this to true
pref("general.useragent.updates.enabled", false);
pref("general.useragent.updates.enabled", true);
pref("general.useragent.updates.url", "https://dynamicua.cdn.mozilla.net/0/%APP_ID%");
pref("general.useragent.updates.interval", 604800); // 1 week
pref("general.useragent.updates.retry", 86400); // 1 day

Просмотреть файл

@ -124,12 +124,14 @@ public class AppConstants {
public static final String USER_AGENT_BOT_LIKE = "Redirector/" + AppConstants.MOZ_APP_VERSION +
" (Android; rv:" + AppConstants.MOZ_APP_VERSION + ")";
public static final String USER_AGENT_FENNEC_MOBILE = "Mozilla/5.0 (Android; Mobile; rv:" +
public static final String USER_AGENT_FENNEC_MOBILE = "Mozilla/5.0 (Android " +
Build.VERSION.RELEASE + "; Mobile; rv:" +
AppConstants.MOZ_APP_VERSION + ") Gecko/" +
AppConstants.MOZ_APP_VERSION + " Firefox/" +
AppConstants.MOZ_APP_VERSION;
public static final String USER_AGENT_FENNEC_TABLET = "Mozilla/5.0 (Android; Tablet; rv:" +
public static final String USER_AGENT_FENNEC_TABLET = "Mozilla/5.0 (Android " +
Build.VERSION.RELEASE + "; Tablet; rv:" +
AppConstants.MOZ_APP_VERSION + ") Gecko/" +
AppConstants.MOZ_APP_VERSION + " Firefox/" +
AppConstants.MOZ_APP_VERSION;

Просмотреть файл

@ -46,6 +46,7 @@ import org.mozilla.gecko.home.HomePager.OnUrlOpenInBackgroundListener;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
import org.mozilla.gecko.home.HomePanelsManager;
import org.mozilla.gecko.home.SearchEngine;
import org.mozilla.gecko.javaaddons.JavaAddonManager;
import org.mozilla.gecko.menu.GeckoMenu;
import org.mozilla.gecko.menu.GeckoMenuItem;
import org.mozilla.gecko.mozglue.ContextUtils;

Просмотреть файл

@ -158,8 +158,14 @@ public final class EventDispatcher {
}
if (listeners != null) {
if (listeners.size() == 0) {
if (listeners.isEmpty()) {
Log.w(LOGTAG, "No listeners for " + type);
// There were native listeners, and they're gone. Dispatch an error rather than
// looking for JSON listeners.
if (callback != null) {
callback.sendError("No listeners for request");
}
}
try {
for (final NativeEventListener listener : listeners) {
@ -195,7 +201,7 @@ public final class EventDispatcher {
synchronized (mGeckoThreadJSONListeners) {
listeners = mGeckoThreadJSONListeners.get(type);
}
if (listeners == null || listeners.size() == 0) {
if (listeners == null || listeners.isEmpty()) {
Log.w(LOGTAG, "No listeners for " + type);
// If there are no listeners, dispatch an error.

Просмотреть файл

@ -105,6 +105,7 @@ ALL_JARS = \
gecko-thirdparty.jar \
gecko-util.jar \
sync-thirdparty.jar \
../javaaddons/javaaddons-1.0.jar \
$(NULL)
ifdef MOZ_WEBRTC

Просмотреть файл

@ -3,20 +3,18 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko;
import org.mozilla.gecko.util.GeckoEventListener;
import org.json.JSONException;
import org.json.JSONObject;
package org.mozilla.gecko.javaaddons;
import android.content.Context;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import dalvik.system.DexClassLoader;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.util.GeckoEventListener;
import java.io.File;
import java.lang.reflect.Constructor;
@ -47,7 +45,7 @@ import java.util.Map;
* dispatcher, they can do so by inserting the response string into the bundle
* under the key "response".
*/
class JavaAddonManager implements GeckoEventListener {
public class JavaAddonManager implements GeckoEventListener {
private static final String LOGTAG = "GeckoJavaAddonManager";
private static JavaAddonManager sInstance;
@ -69,7 +67,7 @@ class JavaAddonManager implements GeckoEventListener {
mAddonCallbacks = new HashMap<String, Map<String, GeckoEventListener>>();
}
void init(Context applicationContext) {
public void init(Context applicationContext) {
if (mApplicationContext != null) {
// we've already done this registration. don't do it again
return;
@ -78,6 +76,7 @@ class JavaAddonManager implements GeckoEventListener {
mDispatcher.registerGeckoThreadListener(this,
"Dex:Load",
"Dex:Unload");
JavaAddonManagerV1.getInstance().init(applicationContext);
}
@Override

Просмотреть файл

@ -0,0 +1,260 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.javaaddons;
import android.content.Context;
import android.support.v4.util.Pair;
import android.util.Log;
import dalvik.system.DexClassLoader;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.util.GeckoJarReader;
import org.mozilla.gecko.util.GeckoRequest;
import org.mozilla.gecko.util.NativeEventListener;
import org.mozilla.gecko.util.NativeJSObject;
import org.mozilla.javaaddons.JavaAddonInterfaceV1;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Map;
public class JavaAddonManagerV1 implements NativeEventListener {
private static final String LOGTAG = "GeckoJavaAddonMgrV1";
public static final String MESSAGE_LOAD = "JavaAddonManagerV1:Load";
public static final String MESSAGE_UNLOAD = "JavaAddonManagerV1:Unload";
private static JavaAddonManagerV1 sInstance;
// Protected by static synchronized.
private Context mApplicationContext;
private final org.mozilla.gecko.EventDispatcher mDispatcher;
// Protected by synchronized(this).
private final Map<String, EventDispatcherImpl> mGUIDToDispatcherMap = new HashMap<>();
public static synchronized JavaAddonManagerV1 getInstance() {
if (sInstance == null) {
sInstance = new JavaAddonManagerV1();
}
return sInstance;
}
private JavaAddonManagerV1() {
mDispatcher = org.mozilla.gecko.EventDispatcher.getInstance();
}
public synchronized void init(Context applicationContext) {
if (mApplicationContext != null) {
// We've already registered; don't register again.
return;
}
mApplicationContext = applicationContext;
mDispatcher.registerGeckoThreadListener(this,
MESSAGE_LOAD,
MESSAGE_UNLOAD);
}
protected String getExtension(String filename) {
if (filename == null) {
return "";
}
final int last = filename.lastIndexOf(".");
if (last < 0) {
return "";
}
return filename.substring(last);
}
protected synchronized EventDispatcherImpl registerNewInstance(String classname, String filename)
throws ClassNotFoundException, InstantiationException, IllegalAccessException, InvocationTargetException, NoSuchMethodException, IOException {
Log.d(LOGTAG, "Attempting to instantiate " + classname + "from filename " + filename);
// It's important to maintain the extension, either .dex, .apk, .jar.
final String extension = getExtension(filename);
final File dexFile = GeckoJarReader.extractStream(mApplicationContext, filename, mApplicationContext.getCacheDir(), "." + extension);
try {
if (dexFile == null) {
throw new IOException("Could not find file " + filename);
}
final File tmpDir = mApplicationContext.getDir("dex", 0); // We'd prefer getCodeCacheDir but it's API 21+.
final DexClassLoader loader = new DexClassLoader(dexFile.getAbsolutePath(), tmpDir.getAbsolutePath(), null, mApplicationContext.getClassLoader());
final Class<?> c = loader.loadClass(classname);
final Constructor<?> constructor = c.getDeclaredConstructor(Context.class, JavaAddonInterfaceV1.EventDispatcher.class);
final String guid = Utils.generateGuid();
final EventDispatcherImpl dispatcher = new EventDispatcherImpl(guid, filename);
final Object instance = constructor.newInstance(mApplicationContext, dispatcher);
mGUIDToDispatcherMap.put(guid, dispatcher);
return dispatcher;
} finally {
// DexClassLoader writes an optimized version, so we can get rid of our temporary extracted version.
if (dexFile != null) {
dexFile.delete();
}
}
}
@Override
public synchronized void handleMessage(String event, NativeJSObject message, org.mozilla.gecko.util.EventCallback callback) {
try {
switch (event) {
case MESSAGE_LOAD: {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}
final String classname = message.getString("classname");
final String filename = message.getString("filename");
final EventDispatcherImpl dispatcher = registerNewInstance(classname, filename);
callback.sendSuccess(dispatcher.guid);
}
break;
case MESSAGE_UNLOAD: {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}
final String guid = message.getString("guid");
final EventDispatcherImpl dispatcher = mGUIDToDispatcherMap.remove(guid);
if (dispatcher == null) {
Log.w(LOGTAG, "Attempting to unload addon with unknown associated dispatcher; ignoring.");
callback.sendSuccess(false);
}
dispatcher.unregisterAllEventListeners();
callback.sendSuccess(true);
}
break;
}
} catch (Exception e) {
Log.e(LOGTAG, "Exception handling message [" + event + "]", e);
if (callback != null) {
callback.sendError("Exception handling message [" + event + "]: " + e.toString());
}
}
}
/**
* An event dispatcher is tied to a single Java Addon instance. It serves to prefix all
* messages with its unique GUID.
* <p/>
* Curiously, the dispatcher does not hold a direct reference to its add-on instance. It will
* likely hold indirect instances through its wrapping map, since the instance will probably
* register event listeners that hold a reference to itself. When these listeners are
* unregistered, any link will be broken, allowing the instances to be garbage collected.
*/
private class EventDispatcherImpl implements JavaAddonInterfaceV1.EventDispatcher {
private final String guid;
private final String dexFileName;
// Protected by synchronized(this).
private final Map<JavaAddonInterfaceV1.EventListener, Pair<NativeEventListener, String[]>> mListenerToWrapperMap = new IdentityHashMap<>();
public EventDispatcherImpl(String guid, String dexFileName) {
this.guid = guid;
this.dexFileName = dexFileName;
}
protected class ListenerWrapper implements NativeEventListener {
private final JavaAddonInterfaceV1.EventListener listener;
public ListenerWrapper(JavaAddonInterfaceV1.EventListener listener) {
this.listener = listener;
}
@Override
public void handleMessage(String prefixedEvent, NativeJSObject message, final org.mozilla.gecko.util.EventCallback callback) {
if (!prefixedEvent.startsWith(guid + ":")) {
return;
}
final String event = prefixedEvent.substring(guid.length() + 1); // Skip "guid:".
try {
JavaAddonInterfaceV1.EventCallback callbackAdapter = null;
if (callback != null) {
callbackAdapter = new JavaAddonInterfaceV1.EventCallback() {
@Override
public void sendSuccess(Object response) {
callback.sendSuccess(response);
}
@Override
public void sendError(Object response) {
callback.sendError(response);
}
};
}
final JSONObject json = new JSONObject(message.toString());
listener.handleMessage(mApplicationContext, event, json, callbackAdapter);
} catch (Exception e) {
Log.e(LOGTAG, "Exception handling message [" + prefixedEvent + "]", e);
if (callback != null) {
callback.sendError("Got exception handling message [" + prefixedEvent + "]: " + e.toString());
}
}
}
}
@Override
public synchronized void registerEventListener(final JavaAddonInterfaceV1.EventListener listener, String... events) {
if (mListenerToWrapperMap.containsKey(listener)) {
Log.e(LOGTAG, "Attempting to register listener which is already registered; ignoring.");
return;
}
final NativeEventListener listenerWrapper = new ListenerWrapper(listener);
final String[] prefixedEvents = new String[events.length];
for (int i = 0; i < events.length; i++) {
prefixedEvents[i] = this.guid + ":" + events[i];
}
mDispatcher.registerGeckoThreadListener(listenerWrapper, prefixedEvents);
mListenerToWrapperMap.put(listener, new Pair<>(listenerWrapper, prefixedEvents));
}
@Override
public synchronized void unregisterEventListener(final JavaAddonInterfaceV1.EventListener listener) {
final Pair<NativeEventListener, String[]> pair = mListenerToWrapperMap.remove(listener);
if (pair == null) {
Log.e(LOGTAG, "Attempting to unregister listener which is not registered; ignoring.");
return;
}
mDispatcher.unregisterGeckoThreadListener(pair.first, pair.second);
}
protected synchronized void unregisterAllEventListeners() {
// Unregister everything, then forget everything.
for (Pair<NativeEventListener, String[]> pair : mListenerToWrapperMap.values()) {
mDispatcher.unregisterGeckoThreadListener(pair.first, pair.second);
}
mListenerToWrapperMap.clear();
}
@Override
public void sendRequestToGecko(final String event, final JSONObject message, final JavaAddonInterfaceV1.RequestCallback callback) {
final String prefixedEvent = guid + ":" + event;
GeckoAppShell.sendRequestToGecko(new GeckoRequest(prefixedEvent, message) {
@Override
public void onResponse(NativeJSObject nativeJSObject) {
if (callback == null) {
// Nothing to do.
return;
}
try {
final JSONObject json = new JSONObject(nativeJSObject.toString());
callback.onResponse(GeckoAppShell.getContext(), json);
} catch (JSONException e) {
// No way to report failure.
Log.e(LOGTAG, "Exception handling response to request [" + event + "]:", e);
}
}
});
}
}
}

Просмотреть файл

@ -357,7 +357,8 @@ gbjar.sources += [
'home/TwoLinePageRow.java',
'InputMethods.java',
'IntentHelper.java',
'JavaAddonManager.java',
'javaaddons/JavaAddonManager.java',
'javaaddons/JavaAddonManagerV1.java',
'LayoutInterceptor.java',
'LocaleManager.java',
'Locales.java',
@ -597,6 +598,7 @@ if CONFIG['MOZ_ANDROID_READING_LIST_SERVICE']:
gbjar.sources += sync_java_files
gbjar.extra_jars += [
OBJDIR + '/../javaaddons/javaaddons-1.0.jar',
'gecko-R.jar',
'gecko-mozglue.jar',
'gecko-thirdparty.jar',

Просмотреть файл

@ -5,21 +5,23 @@
package org.mozilla.gecko.util;
import android.content.Context;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.mozglue.GeckoLoader;
import org.mozilla.gecko.mozglue.NativeZip;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.util.Log;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.mozglue.GeckoLoader;
import org.mozilla.gecko.mozglue.NativeZip;
import org.mozilla.gecko.mozglue.RobocopTarget;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.Stack;
@ -107,6 +109,68 @@ public final class GeckoJarReader {
return new NativeZip(fileUrl.getPath());
}
@RobocopTarget
/**
* Extract a (possibly nested) file from an archive and write it to a temporary file.
*
* @param context Android context.
* @param url to open. Can include jar: to "reach into" nested archives.
* @param dir to write temporary file to.
* @return a <code>File</code>, if one could be written; otherwise null.
* @throws IOException if an error occured.
*/
public static File extractStream(Context context, String url, File dir, String suffix) throws IOException {
InputStream input = null;
try {
try {
final URI fileURI = new URI(url);
// We don't check the scheme because we want to catch bare files, not just file:// URIs.
// If we let bare files through, we'd try to open them as ZIP files later -- and crash in native code.
if (fileURI != null && fileURI.getPath() != null) {
final File inputFile = new File(fileURI.getPath());
if (inputFile != null && inputFile.exists()) {
input = new FileInputStream(inputFile);
}
}
} catch (URISyntaxException e) {
// Not a file:// URI.
}
if (input == null) {
// No luck with file:// URI; maybe some other URI?
input = getStream(context, url);
}
if (input == null) {
// Not found!
return null;
}
// n.b.: createTempFile does not in fact delete the file.
final File file = File.createTempFile("extractStream", suffix, dir);
OutputStream output = null;
try {
output = new FileOutputStream(file);
byte[] buf = new byte[8192];
int len;
while ((len = input.read(buf)) >= 0) {
output.write(buf, 0, len);
}
return file;
} finally {
if (output != null) {
output.close();
}
}
} finally {
if (input != null) {
try {
input.close();
} catch (IOException e) {
Log.w(LOGTAG, "Got exception closing stream; ignoring.", e);
}
}
}
}
@RobocopTarget
public static InputStream getStream(Context context, String url) {
Stack<String> jarUrls = parseUrl(url);

Просмотреть файл

@ -7,10 +7,26 @@
const Ci = Components.interfaces;
const Cu = Components.utils;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
if (!PrivateBrowsingUtils.isContentWindowPrivate(window)) {
document.addEventListener("DOMContentLoaded", function () {
XPCOMUtils.defineLazyGetter(window, "gChromeWin", function()
window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShellTreeItem)
.rootTreeItem
.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindow)
.QueryInterface(Ci.nsIDOMChromeWindow));
document.addEventListener("DOMContentLoaded", function() {
let BrowserApp = gChromeWin.BrowserApp;
if (!PrivateBrowsingUtils.isContentWindowPrivate(window)) {
document.body.setAttribute("class", "normal");
document.getElementById("newPrivateTabLink").addEventListener("click", function() {
BrowserApp.addTab("about:privatebrowsing", { selected: true, parentId: BrowserApp.selectedTab.id, isPrivate: true });
}, false);
}
}, false);
}

Просмотреть файл

@ -21,20 +21,22 @@
<meta name="viewport" content="width=device-width, initial-scale=1; user-scalable=no"/>
<link rel="stylesheet" href="chrome://browser/skin/aboutPrivateBrowsing.css" type="text/css" media="all"/>
<link rel="icon" type="image/png" href="chrome://branding/content/favicon32.png" />
<script type="application/javascript;version=1.8" src="chrome://browser/content/aboutPrivateBrowsing.js"></script>
</head>
<body class="private">
<img class="showPrivate" src="chrome://browser/skin/images/privatebrowsing-mask.png" />
<img class="masq" src="chrome://browser/skin/images/privatebrowsing-mask.png" />
<h1 class="showPrivate">&privatebrowsingpage.title;</h1>
<h1 class="showNormal">&privatebrowsingpage.issueDesc.normal;</h1>
<h1 class="showNormal">&privatebrowsingpage.title.normal;</h1>
<div class="contentSection">
<p class="showPrivate">&privatebrowsingpage.description.private;</p>
<p class="showNormal">&privatebrowsingpage.description;</p>
<p class="showNormal">&privatebrowsingpage.description.normal;</p>
<p>&privatebrowsingpage.moreInfo;</p>
<p class="showPrivate"><a href="https://support.mozilla.org/kb/private-browsing-firefox-android">&privatebrowsingpage.link.private;</a></p>
<p class="showNormal"><a href="#" id="newPrivateTabLink">&privatebrowsingpage.link.normal;</a></p>
</div>
<script type="application/javascript;version=1.8" src="chrome://browser/content/aboutPrivateBrowsing.js"></script>
</body>
</html>

Просмотреть файл

@ -210,6 +210,15 @@
*;
}
# Keep all interfaces that might be dynamically required by Java Addons.
-keep class org.mozilla.javaaddons.* {
*;
}
-keep class org.mozilla.javaaddons.*$* {
*;
}
# Disable obfuscation because it makes exception stack traces more difficult to read.
-dontobfuscate

Просмотреть файл

@ -37,6 +37,7 @@ android {
srcDir "${topobjdir}/mobile/android/gradle/app/src/robocop"
srcDir "${topobjdir}/mobile/android/gradle/app/src/background"
srcDir "${topobjdir}/mobile/android/gradle/app/src/browser"
srcDir "${topobjdir}/mobile/android/gradle/app/src/javaaddons"
}
}
}

Просмотреть файл

@ -0,0 +1,10 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
include $(topsrcdir)/config/rules.mk
JAVA_CLASSPATH := $(ANDROID_SDK)/android.jar
include $(topsrcdir)/config/android-common.mk
libs:: javaaddons-1.0.jar

Просмотреть файл

@ -0,0 +1,51 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.javaaddons;
import android.content.Context;
import org.json.JSONObject;
public interface JavaAddonInterfaceV1 {
/**
* Callback interface for Gecko requests.
* <p/>
* For each instance of EventCallback, exactly one of sendResponse, sendError, must be called to prevent observer leaks.
* If more than one send* method is called, or if a single send method is called multiple times, an
* {@link IllegalStateException} will be thrown.
*/
interface EventCallback {
/**
* Sends a success response with the given data.
*
* @param response The response data to send to Gecko. Can be any of the types accepted by
* JSONObject#put(String, Object).
*/
public void sendSuccess(Object response);
/**
* Sends an error response with the given data.
*
* @param response The response data to send to Gecko. Can be any of the types accepted by
* JSONObject#put(String, Object).
*/
public void sendError(Object response);
}
interface EventDispatcher {
void registerEventListener(EventListener listener, String... events);
void unregisterEventListener(EventListener listener);
void sendRequestToGecko(String event, JSONObject message, RequestCallback callback);
}
interface EventListener {
public void handleMessage(final Context context, final String event, final JSONObject message, final EventCallback callback);
}
interface RequestCallback {
void onResponse(final Context context, JSONObject jsonObject);
}
}

Просмотреть файл

@ -0,0 +1,11 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
jar = add_java_jar('javaaddons-1.0')
jar.sources = [
'java/org/mozilla/javaaddons/JavaAddonInterfaceV1.java',
]
jar.javac_flags += ['-Xlint:all']

Просмотреть файл

@ -3,11 +3,14 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!ENTITY privatebrowsingpage.title "Private Browsing">
<!ENTITY privatebrowsingpage.title.normal "You are not in private browsing">
<!ENTITY privatebrowsingpage.issueDesc.private "You have opened a new private tab.">
<!ENTITY privatebrowsingpage.issueDesc.normal "You are not in a private tab.">
<!ENTITY privatebrowsingpage.issueDesc.normal2 "In private browsing, we won't keep any of your browsing history or cookies.">
<!ENTITY privatebrowsingpage.issueDesc.private2 "We won't keep any of your browsing history or cookies.">
<!ENTITY privatebrowsingpage.description "In private tabs, &brandShortName; won't keep any browser history, search history, download history, web form history, cookies, or temporary internet files. However, files you download and bookmarks you make will be kept.">
<!ENTITY privatebrowsingpage.description.private "&privatebrowsingpage.issueDesc.private; &privatebrowsingpage.description;">
<!ENTITY privatebrowsingpage.description2 "Bookmarks you add and files you download will still be saved on your device.">
<!ENTITY privatebrowsingpage.description.normal "&privatebrowsingpage.issueDesc.normal2; &privatebrowsingpage.description2;">
<!ENTITY privatebrowsingpage.description.private "&privatebrowsingpage.issueDesc.private2; &privatebrowsingpage.description2;">
<!ENTITY privatebrowsingpage.moreInfo "While this device won't have a record of your browsing history, your internet service provider can still track the pages you visit.">
<!ENTITY privatebrowsingpage.link.private "Want to learn more?">
<!ENTITY privatebrowsingpage.link.normal "Open a new private tab">

Просмотреть файл

@ -126,6 +126,7 @@ class MachCommands(MachCommandBase):
srcdir('app/src/robocop/org/mozilla/gecko/tests', 'mobile/android/tests/browser/robocop')
srcdir('app/src/background/org/mozilla/gecko', 'mobile/android/tests/background/junit3/src')
srcdir('app/src/browser', 'mobile/android/tests/browser/junit3/src')
srcdir('app/src/javaaddons', 'mobile/android/tests/javaaddons/src')
# Test libraries.
srcdir('app/libs', 'build/mobile/robocop')
@ -135,6 +136,7 @@ class MachCommands(MachCommandBase):
srcdir('base/src/main/java/org/mozilla/gecko', 'mobile/android/base')
srcdir('base/src/main/java/org/mozilla/mozstumbler', 'mobile/android/stumbler/java/org/mozilla/mozstumbler')
srcdir('base/src/main/java/org/mozilla/search', 'mobile/android/search/java/org/mozilla/search')
srcdir('base/src/main/java/org/mozilla/javaaddons', 'mobile/android/javaaddons/java/org/mozilla/javaaddons')
srcdir('base/src/main/res', 'mobile/android/base/resources')
srcdir('base/src/crashreporter/res', 'mobile/android/base/crashreporter/res')

Просмотреть файл

@ -0,0 +1,115 @@
// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["JavaAddonManager"];
const { classes: Cc, interfaces: Ci, utils: Cu } = Components; /*global Components */
Cu.import("resource://gre/modules/Messaging.jsm"); /*global Messaging */
Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
function resolveGeckoURI(uri) {
if (!uri) {
throw new Error("Can't resolve an empty uri");
}
if (uri.startsWith("chrome://")) {
let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]);
return registry.convertChromeURL(Services.io.newURI(uri, null, null)).spec;
} else if (uri.startsWith("resource://")) {
let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
return handler.resolveURI(Services.io.newURI(uri, null, null));
}
return uri;
}
/**
* A promise-based API
*/
let JavaAddonManager = Object.freeze({
classInstanceFromFile: function(classname, filename) {
if (!classname) {
throw new Error("classname cannot be null");
}
if (!filename) {
throw new Error("filename cannot be null");
}
return Messaging.sendRequestForResult({
type: "JavaAddonManagerV1:Load",
classname: classname,
filename: resolveGeckoURI(filename)
})
.then((guid) => {
if (!guid) {
throw new Error("Internal error: guid should not be null");
}
return new JavaAddonV1({classname: classname, guid: guid});
});
}
});
function JavaAddonV1(options = {}) {
if (!(this instanceof JavaAddonV1)) {
return new JavaAddonV1(options);
}
if (!options.classname) {
throw new Error("options.classname cannot be null");
}
if (!options.guid) {
throw new Error("options.guid cannot be null");
}
this._classname = options.classname;
this._guid = options.guid;
this._loaded = true;
this._listeners = {};
}
JavaAddonV1.prototype = Object.freeze({
unload: function() {
if (!this._loaded) {
return;
}
Messaging.sendRequestForResult({
type: "JavaAddonManagerV1:Unload",
guid: this._guid
})
.then(() => {
this._loaded = false;
for (let listener of this._listeners) {
// If we use this.removeListener, we prefix twice.
Messaging.removeListener(listener);
}
this._listeners = {};
});
},
_prefix: function(message) {
let newMessage = Cu.cloneInto(message, {}, { cloneFunctions: false });
newMessage.type = this._guid + ":" + message.type;
return newMessage;
},
sendRequest: function(message) {
return Messaging.sendRequest(this._prefix(message));
},
sendRequestForResult: function(message) {
return Messaging.sendRequestForResult(this._prefix(message));
},
addListener: function(listener, message) {
let prefixedMessage = this._guid + ":" + message;
this._listeners[prefixedMessage] = listener;
return Messaging.addListener(listener, prefixedMessage);
},
removeListener: function(message) {
let prefixedMessage = this._guid + ":" + message;
delete this._listeners[prefixedMessage];
return Messaging.removeListener(prefixedMessage);
}
});

Просмотреть файл

@ -14,6 +14,7 @@ EXTRA_JS_MODULES += [
'HelperApps.jsm',
'Home.jsm',
'HomeProvider.jsm',
'JavaAddonManager.jsm',
'JNI.jsm',
'LightweightThemeConsumer.jsm',
'MatchstickApp.jsm',

Просмотреть файл

@ -15,6 +15,7 @@ if CONFIG['MOZ_ANDROID_MLS_STUMBLER']:
DIRS += ['stumbler']
DIRS += [
'javaaddons', # Must be built before base.
'base',
'chrome',
'components',

Просмотреть файл

@ -3,10 +3,14 @@
package org.mozilla.tests.browser.junit3;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Stack;
import android.test.InstrumentationTestCase;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.util.FileUtils;
import org.mozilla.gecko.util.GeckoJarReader;
import android.content.Context;
@ -46,4 +50,69 @@ public class TestJarReader extends InstrumentationTestCase {
stream = GeckoJarReader.getStream(context, "jar:" + url + "!/chrome/chrome/content/branding/favicon32.png");
assertNull(stream);
}
protected void assertExtractStream(String url) throws IOException {
final File file = GeckoJarReader.extractStream(getInstrumentation().getTargetContext(), url, getInstrumentation().getContext().getCacheDir(), ".test");
assertNotNull(file);
try {
assertTrue(file.getName().endsWith("temp"));
final String contents = FileUtils.getFileContents(file);
assertNotNull(contents);
assertTrue(contents.length() > 0);
} finally {
file.delete();
}
}
public void testExtractStream() throws IOException {
String appPath = getInstrumentation().getTargetContext().getPackageResourcePath();
assertNotNull(appPath);
// We don't have a lot of good files to choose from. package-name.txt isn't included in Gradle APKs.
assertExtractStream("jar:file://" + appPath + "!/resources.arsc");
final String url = GeckoJarReader.getJarURL(getInstrumentation().getTargetContext(), "chrome.manifest");
assertExtractStream(url);
// Now use an extracted copy of chrome.manifest to test further.
final File file = GeckoJarReader.extractStream(getInstrumentation().getTargetContext(), url, getInstrumentation().getContext().getCacheDir(), ".test");
assertNotNull(file);
try {
assertExtractStream("file://" + file.getAbsolutePath()); // file:// URI.
assertExtractStream(file.getAbsolutePath()); // Vanilla path.
} finally {
file.delete();
}
}
protected void assertExtractStreamFails(String url) throws IOException {
final File file = GeckoJarReader.extractStream(getInstrumentation().getTargetContext(), url, getInstrumentation().getContext().getCacheDir(), ".test");
assertNull(file);
}
public void testExtractStreamFailureCases() throws IOException {
String appPath = getInstrumentation().getTargetContext().getPackageResourcePath();
assertNotNull(appPath);
// First, a bad APK.
assertExtractStreamFails("jar:file://" + appPath + "BAD!/resources.arsc");
// Second, a bad file in the APK.
assertExtractStreamFails("jar:file://" + appPath + "!/BADresources.arsc");
// Now a bad file in the omnijar.
final String badUrl = GeckoJarReader.getJarURL(getInstrumentation().getTargetContext(), "BADchrome.manifest");
assertExtractStreamFails(badUrl);
// Now use an extracted copy of chrome.manifest to test further.
final String goodUrl = GeckoJarReader.getJarURL(getInstrumentation().getTargetContext(), "chrome.manifest");
final File file = GeckoJarReader.extractStream(getInstrumentation().getTargetContext(), goodUrl, getInstrumentation().getContext().getCacheDir(), ".test");
assertNotNull(file);
try {
assertExtractStreamFails("file://" + file.getAbsolutePath() + "BAD"); // Bad file:// URI.
assertExtractStreamFails(file.getAbsolutePath() + "BAD"); //Bad vanilla path.
} finally {
file.delete();
}
}
}

Просмотреть файл

@ -6,4 +6,5 @@
TEST_DIRS += [
'junit3',
'robocop/roboextender',
]

Просмотреть файл

@ -123,6 +123,7 @@ skip-if = android_version == "18"
[testHistoryService.java]
# disabled on 4.3, bug 1116036
skip-if = android_version == "18"
[testJavaAddons.java]
[testJNI.java]
# [testMozPay.java] # see bug 945675
[testMigrateUI.java]

Просмотреть файл

@ -19,6 +19,7 @@ INSTALL_TARGETS += TEST
include $(topsrcdir)/config/rules.mk
libs:: $(_TEST_FILES)
tools:: $(_TEST_FILES)
$(MKDIR) -p $(TEST_EXTENSIONS_DIR)/roboextender@mozilla.org/base
-cp $(TESTPATH)/* $(TEST_EXTENSIONS_DIR)/roboextender@mozilla.org/base
-cp $(TESTPATH)/base/* $(TEST_EXTENSIONS_DIR)/roboextender@mozilla.org/base
-cp $(DEPTH)/mobile/android/tests/javaaddons/javaaddons-test.apk $(TEST_EXTENSIONS_DIR)/roboextender@mozilla.org/base

Просмотреть файл

Просмотреть файл

@ -0,0 +1,13 @@
/* 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.tests;
public class testJavaAddons extends JavascriptTest {
public testJavaAddons() {
super("testJavaAddons.js");
}
}

Просмотреть файл

@ -0,0 +1,97 @@
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
/* 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 { classes: Cc, interfaces: Ci, utils: Cu } = Components;
let Log = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.bind("TestJavaAddons");
Cu.import("resource://gre/modules/JavaAddonManager.jsm"); /*global JavaAddonManager */
Cu.import("resource://gre/modules/Promise.jsm"); /*global Promise */
Cu.import("resource://gre/modules/Services.jsm"); /*global Services */
Cu.import("resource://gre/modules/Messaging.jsm"); /*global Messaging */
const DEX_FILE = "chrome://roboextender/content/javaaddons-test.apk";
const CLASS = "org.mozilla.javaaddons.test.JavaAddonV1";
const MESSAGE = "JavaAddon:V1";
add_task(function testFailureCases() {
do_print("Loading Java Addon from non-existent class.");
let gotError1 = yield JavaAddonManager.classInstanceFromFile(CLASS + "GARBAGE", DEX_FILE)
.then((result) => false)
.catch((error) => true);
do_check_eq(gotError1, true);
do_print("Loading Java Addon from non-existent DEX file.");
let gotError2 = yield JavaAddonManager.classInstanceFromFile(CLASS, DEX_FILE + "GARBAGE")
.then((result) => false)
.catch((error) => true);
do_check_eq(gotError2, true);
});
// Make a request to a dynamically loaded Java Addon; wait for a response.
// Then expect the add-on to make a request; respond.
// Then expect the add-on to make a second request; use it to verify the response to the first request.
add_task(function testJavaAddonV1() {
do_print("Loading Java Addon from: " + DEX_FILE);
let javaAddon = yield JavaAddonManager.classInstanceFromFile(CLASS, DEX_FILE);
do_check_neq(javaAddon, null);
do_check_neq(javaAddon._guid, null);
do_check_eq(javaAddon._classname, CLASS);
do_check_eq(javaAddon._loaded, true);
let messagePromise = Promise.defer();
var count = 0;
function listener(data) {
do_print("Got request initiated from Java Addon: " + data + ", " + typeof(data) + ", " + JSON.stringify(data));
count += 1;
messagePromise.resolve(); // It's okay to resolve before returning: we'll wait on the verification promise no matter what.
return {
outputStringKey: "inputStringKey=" + data.inputStringKey,
outputIntKey: data.inputIntKey - 1
};
}
javaAddon.addListener(listener, "JavaAddon:V1:Request");
let verifierPromise = Promise.defer();
function verifier(data) {
do_print("Got verification request initiated from Java Addon: " + data + ", " + typeof(data) + ", " + JSON.stringify(data));
// These values are from the test Java Addon, after being processed by the :Request listener above.
do_check_eq(data.outputStringKey, "inputStringKey=raw");
do_check_eq(data.outputIntKey, 2);
verifierPromise.resolve();
return {};
}
javaAddon.addListener(verifier, "JavaAddon:V1:VerificationRequest");
let message = {type: MESSAGE, inputStringKey: "test", inputIntKey: 5};
do_print("Sending request to Java Addon: " + JSON.stringify(message));
let output = yield javaAddon.sendRequestForResult(message);
do_print("Got response from Java Addon: " + output + ", " + typeof(output) + ", " + JSON.stringify(output));
do_check_eq(output.outputStringKey, "inputStringKey=test");
do_check_eq(output.outputIntKey, 6);
// We don't worry about timing out: the harness will (very much later)
// kill us if we don't see the expected messages.
do_print("Waiting for request initiated from Java Addon.");
yield messagePromise.promise;
do_check_eq(count, 1);
do_print("Send request for result 2 for request initiated from Java Addon.");
// The JavaAddon should have removed its listener, so we shouldn't get a response and count should stay the same.
let gotError = yield javaAddon.sendRequestForResult(message)
.then((result) => false)
.catch((error) => true);
do_check_eq(gotError, true);
do_check_eq(count, 1);
do_print("Waiting for verification request initiated from Java Addon.");
yield verifierPromise.promise;
});
run_next_test();

Просмотреть файл

@ -0,0 +1,14 @@
#filter substitution
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.mozilla.javaaddons.test"
android:versionCode="1"
android:versionName="1.0" >
<uses-sdk android:minSdkVersion="@MOZ_ANDROID_MIN_SDK_VERSION@"
#ifdef MOZ_ANDROID_MAX_SDK_VERSION
android:maxSdkVersion="@MOZ_ANDROID_MAX_SDK_VERSION@"
#endif
android:targetSdkVersion="@ANDROID_TARGET_SDK@"/>
</manifest>

Просмотреть файл

@ -0,0 +1,16 @@
# 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/.
ANDROID_APK_NAME := javaaddons-test
PP_TARGETS += manifest
manifest := $(srcdir)/AndroidManifest.xml.in
manifest_TARGET := export
ANDROID_MANIFEST_FILE := $(CURDIR)/AndroidManifest.xml
ANDROID_EXTRA_JARS := javaaddons-test.jar
tools libs:: $(ANDROID_APK_NAME).apk
include $(topsrcdir)/config/rules.mk

Просмотреть файл

@ -0,0 +1,16 @@
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
jar = add_java_jar('javaaddons-test')
jar.extra_jars += [
TOPOBJDIR + '/mobile/android/javaaddons/javaaddons-1.0.jar',
]
jar.javac_flags += ['-Xlint:all']
jar.sources += [
'src/org/mozilla/javaaddons/test/ClassWithNoRecognizedConstructors.java',
'src/org/mozilla/javaaddons/test/JavaAddonV0.java',
'src/org/mozilla/javaaddons/test/JavaAddonV1.java',
]

Просмотреть файл

@ -0,0 +1,3 @@
<resources>
<string name="app_name">org.mozilla.javaaddons.test</string>
</resources>

Просмотреть файл

@ -0,0 +1,11 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.javaaddons.test;
public class ClassWithNoRecognizedConstructors {
public ClassWithNoRecognizedConstructors(int a, String b, boolean c) {
}
}

Просмотреть файл

@ -0,0 +1,24 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.javaaddons.test;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import java.util.Map;
public class JavaAddonV0 implements Handler.Callback {
public JavaAddonV0(Map<String, Handler.Callback> callbacks) {
callbacks.put("JavaAddon:V0", this);
}
@Override
public boolean handleMessage(Message message) {
Log.i("JavaAddon", "handleMessage " + message.toString());
return true;
}
}

Просмотреть файл

@ -0,0 +1,59 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; 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.javaaddons.test;
import android.content.Context;
import android.util.Log;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.javaaddons.JavaAddonInterfaceV1.EventCallback;
import org.mozilla.javaaddons.JavaAddonInterfaceV1.EventDispatcher;
import org.mozilla.javaaddons.JavaAddonInterfaceV1.EventListener;
import org.mozilla.javaaddons.JavaAddonInterfaceV1.RequestCallback;
public class JavaAddonV1 implements EventListener, RequestCallback {
protected final EventDispatcher mDispatcher;
public JavaAddonV1(Context context, EventDispatcher dispatcher) {
mDispatcher = dispatcher;
mDispatcher.registerEventListener(this, "JavaAddon:V1");
}
@Override
public void handleMessage(Context context, String event, JSONObject message, EventCallback callback) {
Log.i("JavaAddon", "handleMessage: " + event + ", " + message.toString());
final JSONObject output = new JSONObject();
try {
output.put("outputStringKey", "inputStringKey=" + message.getString("inputStringKey"));
output.put("outputIntKey", 1 + message.getInt("inputIntKey"));
} catch (JSONException e) {
// Should never happen; ignore.
}
// Respond.
if (callback != null) {
callback.sendSuccess(output);
}
// And send an independent Gecko event.
final JSONObject input = new JSONObject();
try {
input.put("inputStringKey", "raw");
input.put("inputIntKey", 3);
} catch (JSONException e) {
// Should never happen; ignore.
}
mDispatcher.sendRequestToGecko("JavaAddon:V1:Request", input, this);
}
@Override
public void onResponse(Context context, JSONObject jsonObject) {
Log.i("JavaAddon", "onResponse: " + jsonObject.toString());
// Unregister event listener, so that the JavaScript side can send a test message and
// check it is not handled.
mDispatcher.unregisterEventListener(this);
mDispatcher.sendRequestToGecko("JavaAddon:V1:VerificationRequest", jsonObject, null);
}
}

Просмотреть файл

@ -7,6 +7,8 @@
TEST_DIRS += [
'background',
'browser',
'javaaddons', # Must be built before browser/robocop/roboextender.
# This is enforced in config/recurse.mk.
]
ANDROID_INSTRUMENTATION_MANIFESTS += ['browser/robocop/robocop.ini']

Просмотреть файл

@ -4,7 +4,9 @@
body {
font-family: "Clear Sans",sans-serif;
font-size: 14px;
font-size: 16px;
text-align: center;
padding: 0 30px 0;
}
body.normal .showPrivate,
@ -13,48 +15,52 @@ body.private .showNormal {
}
div.contentSection {
width: 92%;
max-width: 400px;
margin: 0 auto;
margin:auto;
}
body.private {
color: #9ba3ab;
background-color: #292c29;
color: #afb1b3; /* tabs_tray_icon_grey */
}
body.normal {
color: #222;
background-color: #ced7de;
background-color: #eeeeee;
color: #777777; /* placeholder gray */
}
h1 {
font-size: 24px;
font-size: 20px;
font-weight: 100;
text-align: center;
margin: 0;
}
body.private h1 {
color: #d06bff;
body.normal h1 {
color: #363b40; /* text_and_tabs_tray_grey */
}
img {
a {
color: #0096DD; /* link_blue */
text-decoration: none;
}
.masq {
display: block;
width: 125px;
width: 80px;
height: auto;
margin: 0 auto;
margin: 0 auto 20px auto;
}
@media all and (max-height: 399px) {
body {
margin-top: 25px;
margin-top: 30px;
}
}
@media all and (min-height: 400px) and (max-height: 599px) {
body {
margin-top: 50px;
margin-top: 60px;
}
}

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 33 KiB

После

Ширина:  |  Высота:  |  Размер: 2.8 KiB

Просмотреть файл

@ -708,14 +708,32 @@ nsHttpHandler::InitUserAgentComponents()
);
#endif
// Add the `Mobile` or `Tablet` token when running on device or in the
// b2g desktop simulator.
#if defined(ANDROID) || defined(FXOS_SIMULATOR)
nsCOMPtr<nsIPropertyBag2> infoService = do_GetService("@mozilla.org/system-info;1");
MOZ_ASSERT(infoService, "Could not find a system info service");
nsresult rv;
// Add the Android version number to the Fennec platform identifier.
#if defined MOZ_WIDGET_ANDROID
nsAutoString androidVersion;
rv = infoService->GetPropertyAsAString(
NS_LITERAL_STRING("release_version"), androidVersion);
if (NS_SUCCEEDED(rv)) {
mPlatform += " ";
// If the 2nd character is a ".", we know the major version is a single
// digit. If we're running on a version below 4 we pretend to be on
// Android KitKat (4.4) to work around scripts sniffing for low versions.
if (androidVersion[1] == 46 && androidVersion[0] < 52) {
mPlatform += "4.4";
} else {
mPlatform += NS_LossyConvertUTF16toASCII(androidVersion);
}
}
#endif
// Add the `Mobile` or `Tablet` token when running on device or in the
// b2g desktop simulator.
bool isTablet;
nsresult rv = infoService->GetPropertyAsBool(NS_LITERAL_STRING("tablet"), &isTablet);
rv = infoService->GetPropertyAsBool(NS_LITERAL_STRING("tablet"), &isTablet);
if (NS_SUCCEEDED(rv) && isTablet)
mCompatDevice.AssignLiteral("Tablet");
else

Просмотреть файл

@ -11,9 +11,6 @@ DIRS += [
'BrowserTestUtils',
]
if CONFIG['MOZ_BUILD_APP'] == 'mobile/android':
DIRS += ['roboextender']
XPI_NAME = 'mochijar'
JAR_MANIFESTS += ['jar.mn']

Просмотреть файл

@ -426,6 +426,14 @@ exports.defineLazyGetter(this, "NetUtil", () => {
return Cu.import("resource://gre/modules/NetUtil.jsm", {}).NetUtil;
});
exports.defineLazyGetter(this, "OS", () => {
return Cu.import("resource://gre/modules/osfile.jsm", {}).OS;
});
exports.defineLazyGetter(this, "TextDecoder", () => {
return Cu.import("resource://gre/modules/osfile.jsm", {}).TextDecoder;
});
/**
* Performs a request to load the desired URL and returns a promise.
*
@ -438,115 +446,34 @@ exports.defineLazyGetter(this, "NetUtil", () => {
* - policy: the nsIContentPolicy type to apply when fetching the URL
* - window: the window to get the loadGroup from
* - charset: the charset to use if the channel doesn't provide one
* @returns Promise
* A promise of the document at that URL, as a string.
* @returns Promise that resolves with an object with the following members on
* success:
* - content: the document at that URL, as a string,
* - contentType: the content type of the document
*
* If an error occurs, the promise is rejected with that error.
*
* XXX: It may be better to use nsITraceableChannel to get to the sources
* without relying on caching when we can (not for eval, etc.):
* http://www.softwareishard.com/blog/firebug/nsitraceablechannel-intercept-http-traffic/
*/
// Fetch is defined differently depending on whether we are on the main thread
// or a worker thread.
if (!this.isWorker) {
exports.fetch = function (aURL, aOptions={ loadFromCache: true,
function mainThreadFetch(aURL, aOptions={ loadFromCache: true,
policy: Ci.nsIContentPolicy.TYPE_OTHER,
window: null,
charset: null }) {
let deferred = promise.defer();
let scheme;
// Create a channel.
let url = aURL.split(" -> ").pop();
let charset;
let contentType;
try {
scheme = Services.io.extractScheme(url);
} catch (e) {
// In the xpcshell tests, the script url is the absolute path of the test
// file, which will make a malformed URI error be thrown. Add the file
// scheme prefix ourselves.
url = "file://" + url;
scheme = Services.io.extractScheme(url);
}
switch (scheme) {
case "file":
case "chrome":
case "resource":
try {
NetUtil.asyncFetch({
uri: url,
loadUsingSystemPrincipal: true
}, function onFetch(aStream, aStatus, aRequest) {
if (!components.isSuccessCode(aStatus)) {
deferred.reject(new Error("Request failed with status code = "
+ aStatus
+ " after NetUtil.asyncFetch for url = "
+ url));
return;
}
let source = NetUtil.readInputStreamToString(aStream, aStream.available());
contentType = aRequest.contentType;
deferred.resolve(source);
aStream.close();
});
} catch (ex) {
deferred.reject(ex);
}
break;
default:
let channel;
try {
channel = Services.io.newChannel2(url,
null,
null,
null, // aLoadingNode
Services.scriptSecurityManager.getSystemPrincipal(),
null, // aTriggeringPrincipal
Ci.nsILoadInfo.SEC_NORMAL,
aOptions.policy);
} catch (e if e.name == "NS_ERROR_UNKNOWN_PROTOCOL") {
// On Windows xpcshell tests, c:/foo/bar can pass as a valid URL, but
// newChannel won't be able to handle it.
url = "file:///" + url;
channel = Services.io.newChannel2(url,
null,
null,
null, // aLoadingNode
Services.scriptSecurityManager.getSystemPrincipal(),
null, // aTriggeringPrincipal
Ci.nsILoadInfo.SEC_NORMAL,
aOptions.policy);
}
let chunks = [];
let streamListener = {
onStartRequest: function(aRequest, aContext, aStatusCode) {
if (!components.isSuccessCode(aStatusCode)) {
deferred.reject(new Error("Request failed with status code = "
+ aStatusCode
+ " in onStartRequest handler for url = "
+ url));
}
},
onDataAvailable: function(aRequest, aContext, aStream, aOffset, aCount) {
chunks.push(NetUtil.readInputStreamToString(aStream, aCount));
},
onStopRequest: function(aRequest, aContext, aStatusCode) {
if (!components.isSuccessCode(aStatusCode)) {
deferred.reject(new Error("Request failed with status code = "
+ aStatusCode
+ " in onStopRequest handler for url = "
+ url));
return;
channel = newChannelForURL(url, aOptions);
} catch (ex) {
return promise.reject(ex);
}
charset = channel.contentCharset || aOptions.charset;
contentType = channel.contentType;
deferred.resolve(chunks.join(""));
}
};
// Set the channel options.
channel.loadFlags = aOptions.loadFromCache
? channel.LOAD_FROM_CACHE
: channel.LOAD_BYPASS_CACHE;
if (aOptions.window) {
// Respect private browsing.
@ -555,27 +482,94 @@ if (!this.isWorker) {
.QueryInterface(Ci.nsIDocumentLoader)
.loadGroup;
}
channel.loadFlags = aOptions.loadFromCache
? channel.LOAD_FROM_CACHE
: channel.LOAD_BYPASS_CACHE;
try {
channel.asyncOpen(streamListener, null);
} catch(e) {
deferred.reject(new Error("Request failed for '"
+ url
+ "': "
+ e.message));
}
break;
let deferred = promise.defer();
let onResponse = (stream, status, request) => {
if (!components.isSuccessCode(status)) {
deferred.reject(new Error(`Failed to fetch ${url}. Code ${status}.`));
return;
}
return deferred.promise.then(source => {
try {
let charset = channel.contentCharset || aOptions.charset || "UTF-8";
// NetUtil handles charset conversion.
let available = stream.available();
let source = NetUtil.readInputStreamToString(stream, available, {charset});
stream.close();
deferred.resolve({
content: source,
contentType: request.contentType
});
} catch (ex) {
let uri = request.originalURI;
if (ex.name === "NS_BASE_STREAM_CLOSED" && uri instanceof Ci.nsIFileURL) {
// Empty files cause NS_BASE_STREAM_CLOSED exception. Use OS.File to
// differentiate between empty files and other errors (bug 1170864).
// This can be removed when bug 982654 is fixed.
uri.QueryInterface(Ci.nsIFileURL);
let result = OS.File.read(uri.file.path).then(bytes => {
// Convert the bytearray to a String.
let decoder = new TextDecoder();
let content = decoder.decode(bytes);
// We can't detect the contentType without opening a channel
// and that failed already. This is the best we can do here.
return {
content: convertToUnicode(source, charset),
contentType: contentType
content,
contentType: "text/plain"
};
});
deferred.resolve(result);
} else {
deferred.reject(ex);
}
}
};
// Open the channel
try {
NetUtil.asyncFetch(channel, onResponse);
} catch (ex) {
return promise.reject(ex);
}
return deferred.promise;
}
/**
* Opens a channel for given URL. Tries a bit harder than NetUtil.newChannel.
*
* @param {String} url - The URL to open a channel for.
* @param {Object} options - The options object passed to @method fetch.
* @return {nsIChannel} - The newly created channel. Throws on failure.
*/
function newChannelForURL(url, { policy }) {
let channelOptions = {
contentPolicyType: policy,
loadUsingSystemPrincipal: true,
uri: url
};
try {
return NetUtil.newChannel(channelOptions);
} catch (e) {
// In the xpcshell tests, the script url is the absolute path of the test
// file, which will make a malformed URI error be thrown. Add the file
// scheme to see if it helps.
channelOptions.uri = "file://" + url;
return NetUtil.newChannel(channelOptions);
}
}
// Fetch is defined differently depending on whether we are on the main thread
// or a worker thread.
if (!this.isWorker) {
exports.fetch = mainThreadFetch;
} else {
// Services is not available in worker threads, nor is there any other way
// to fetch a URL. We need to enlist the help from the main thread here, by
@ -585,26 +579,6 @@ if (!this.isWorker) {
}
}
/**
* Convert a given string, encoded in a given character set, to unicode.
*
* @param string aString
* A string.
* @param string aCharset
* A character set.
*/
function convertToUnicode(aString, aCharset=null) {
// Decoding primitives.
let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
.createInstance(Ci.nsIScriptableUnicodeConverter);
try {
converter.charset = aCharset || "UTF-8";
return converter.ConvertToUnicode(aString);
} catch(e) {
return aString;
}
}
/**
* Returns a promise that is resolved or rejected when all promises have settled
* (resolved or rejected).

Просмотреть файл

@ -1364,6 +1364,7 @@ function WorkerClient(aClient, aForm) {
this._actor = aForm.from;
this._isClosed = false;
this._isFrozen = aForm.isFrozen;
this._url = aForm.url;
this._onClose = this._onClose.bind(this);
this._onFreeze = this._onFreeze.bind(this);
@ -1387,6 +1388,10 @@ WorkerClient.prototype = {
return this._actor;
},
get url() {
return this._url;
},
get isClosed() {
return this._isClosed;
},

Просмотреть файл

@ -768,6 +768,20 @@ let PageStyleActor = protocol.ActorClass({
}
},
/**
* Get layout-related information about a node.
* This method returns an object with properties giving information about
* the node's margin, border, padding and content region sizes, as well
* as information about the type of box, its position, z-index, etc...
* @param {NodeActor} node
* @param {Object} options The only available option is autoMargins.
* If set to true, the element's margins will receive an extra check to see
* whether they are set to "auto" (knowing that the computed-style in this
* case would return "0px").
* The returned object will contain an extra property (autoMargins) listing
* all margins that are set to auto, e.g. {top: "auto", left: "auto"}.
* @return {Object}
*/
getLayout: method(function(node, options) {
this.cssLogic.highlight(node.rawNode);
@ -795,7 +809,10 @@ let PageStyleActor = protocol.ActorClass({
"border-top-width",
"border-right-width",
"border-bottom-width",
"border-left-width"
"border-left-width",
"z-index",
"box-sizing",
"display"
]) {
layout[prop] = style.getPropertyValue(prop);
}
@ -809,10 +826,6 @@ let PageStyleActor = protocol.ActorClass({
this.map[i].value = parseFloat(style.getPropertyValue(property));
}
if (options.margins) {
layout.margins = this.processMargins(this.cssLogic);
}
return layout;
}, {
request: {

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше