зеркало из https://github.com/mozilla/gecko-dev.git
Merge fx-team to m-c. a=merge
This commit is contained in:
Коммит
38908bda34
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Двоичные данные
mobile/android/themes/core/images/privatebrowsing-mask.png
Двоичные данные
mobile/android/themes/core/images/privatebrowsing-mask.png
Двоичный файл не отображается.
До Ширина: | Высота: | Размер: 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: {
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче