merge fx-team to mozilla-central a=merge

This commit is contained in:
Carsten "Tomcat" Book 2015-06-11 14:41:37 +02:00
commit 3d9b01b5c8
115 changed files with 2008 additions and 772 deletions

View File

@ -7206,11 +7206,6 @@ let gRemoteTabsUI = {
return;
}
if (Services.appinfo.inSafeMode) {
// e10s isn't supported in safe mode, so don't show the menu items for it
return;
}
#ifdef XP_MACOSX
if (Services.prefs.getBoolPref("layers.acceleration.disabled")) {
// On OS X, "Disable Hardware Acceleration" also disables OMTC and forces

View File

@ -1126,7 +1126,7 @@ if (Services.prefs.getBoolPref("browser.pocket.enabled")) {
}
#ifdef E10S_TESTING_ONLY
let e10sDisabled = Services.appinfo.inSafeMode;
let e10sDisabled = false;
#ifdef XP_MACOSX
// On OS X, "Disable Hardware Acceleration" also disables OMTC and forces
// a fallback to Basic Layers. This is incompatible with e10s.

View File

@ -981,12 +981,14 @@ BrowserGlue.prototype = {
this._checkForOldBuildUpdates();
let disabledAddons = AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_DISABLED);
for (let id of disabledAddons) {
if (AddonManager.getAddonByID(id).signedState <= AddonManager.SIGNEDSTATE_MISSING) {
this._notifyUnsignedAddonsDisabled();
break;
AddonManager.getAddonsByIDs(disabledAddons, (addons) => {
for (let addon of addons) {
if (addon.signedState <= AddonManager.SIGNEDSTATE_MISSING) {
this._notifyUnsignedAddonsDisabled();
break;
}
}
}
});
this._firstWindowTelemetry(aWindow);
},

View File

@ -90,7 +90,7 @@ Tools.inspector = {
panelLabel: l10n("inspector.panelLabel", inspectorStrings),
get tooltip() {
return l10n("inspector.tooltip2", inspectorStrings,
( osString == "Darwin" ? "Cmd+Alt" : "Ctrl+Shift+" ) + this.key);
( osString == "Darwin" ? "Cmd+Opt+" : "Ctrl+Shift+" ) + this.key);
},
inMenu: true,
commands: [
@ -127,7 +127,7 @@ Tools.webConsole = {
panelLabel: l10n("ToolboxWebConsole.panelLabel", webConsoleStrings),
get tooltip() {
return l10n("ToolboxWebconsole.tooltip2", webConsoleStrings,
( osString == "Darwin" ? "Cmd+Alt" : "Ctrl+Shift+" ) + this.key);
( osString == "Darwin" ? "Cmd+Opt+" : "Ctrl+Shift+" ) + this.key);
},
inMenu: true,
commands: "devtools/webconsole/console-commands",
@ -163,7 +163,7 @@ Tools.jsdebugger = {
panelLabel: l10n("ToolboxDebugger.panelLabel", debuggerStrings),
get tooltip() {
return l10n("ToolboxDebugger.tooltip2", debuggerStrings,
( osString == "Darwin" ? "Cmd+Alt" : "Ctrl+Shift+" ) + this.key);
( osString == "Darwin" ? "Cmd+Opt+" : "Ctrl+Shift+" ) + this.key);
},
inMenu: true,
commands: "devtools/debugger/debugger-commands",
@ -288,7 +288,7 @@ Tools.netMonitor = {
panelLabel: l10n("netmonitor.panelLabel", netMonitorStrings),
get tooltip() {
return l10n("netmonitor.tooltip2", netMonitorStrings,
( osString == "Darwin" ? "Cmd+Alt" : "Ctrl+Shift+" ) + this.key);
( osString == "Darwin" ? "Cmd+Opt+" : "Ctrl+Shift+" ) + this.key);
},
inMenu: true,

View File

@ -23,21 +23,21 @@ loader.lazyRequireGetter(this, "FrameUtils",
* The raw thread object received from the backend. Contains samples,
* stackTable, frameTable, and stringTable.
* @param object options
* Additional supported options, @see ThreadNode.prototype.insert
* - number startTime [optional]
* - number endTime [optional]
* Additional supported options
* - number startTime
* - number endTime
* - boolean contentOnly [optional]
* - boolean invertTree [optional]
* - boolean flattenRecursion [optional]
*/
function ThreadNode(thread, options = {}) {
if (options.endTime == void 0 || options.startTime == void 0) {
throw new Error("ThreadNode requires both `startTime` and `endTime`.");
}
this.samples = 0;
this.duration = 0;
this.youngestFrameSamples = 0;
this.calls = [];
// Maps of frame to their self counts and duration.
this.selfCount = Object.create(null);
this.selfDuration = Object.create(null);
this.duration = options.endTime - options.startTime;
let { samples, stackTable, frameTable, stringTable, allocationsTable } = thread;
@ -74,8 +74,8 @@ ThreadNode.prototype = {
* index.
* @param object options
* Additional supported options
* - number startTime [optional]
* - number endTime [optional]
* - number startTime
* - number endTime
* - boolean contentOnly [optional]
* - boolean invertTree [optional]
*/
@ -120,9 +120,6 @@ ThreadNode.prototype = {
const InflatedFrame = FrameUtils.InflatedFrame;
const getOrAddInflatedFrame = FrameUtils.getOrAddInflatedFrame;
let selfCount = this.selfCount;
let selfDuration = this.selfDuration;
let samplesData = samples.data;
let stacksData = stackTable.data;
@ -130,8 +127,8 @@ ThreadNode.prototype = {
let inflatedFrameCache = FrameUtils.getInflatedFrameCache(frameTable);
let leafTable = Object.create(null);
let startTime = options.startTime || 0
let endTime = options.endTime || Infinity;
let startTime = options.startTime;
let endTime = options.endTime;
let flattenRecursion = options.flattenRecursion;
// Take the timestamp of the first sample as prevSampleTime. 0 is
@ -163,7 +160,6 @@ ThreadNode.prototype = {
continue;
}
let sampleDuration = sampleTime - prevSampleTime;
let stackIndex = sample[SAMPLE_STACK_SLOT];
let calls = this.calls;
let prevCalls = this.calls;
@ -223,22 +219,9 @@ ThreadNode.prototype = {
mutableFrameKeyOptions.isRoot = stackIndex === null;
let frameKey = inflatedFrame.getFrameKey(mutableFrameKeyOptions);
// Leaf frames are never skipped and require self count and duration
// bookkeeping.
if (isLeaf) {
// Tabulate self count and duration for the leaf frame. The frameKey
// is never empty for a leaf frame.
if (selfCount[frameKey] === undefined) {
selfCount[frameKey] = 0;
selfDuration[frameKey] = 0;
}
selfCount[frameKey]++;
selfDuration[frameKey] += sampleDuration;
} else {
// An empty frame key means this frame should be skipped.
if (frameKey === "") {
continue;
}
// An empty frame key means this frame should be skipped.
if (frameKey === "") {
continue;
}
// If we shouldn't flatten the current frame into the previous one, advance a
@ -250,18 +233,18 @@ ThreadNode.prototype = {
let frameNode = getOrAddFrameNode(calls, isLeaf, frameKey, inflatedFrame,
mutableFrameKeyOptions.isMetaCategoryOut,
leafTable);
frameNode._countSample(prevSampleTime, sampleTime, inflatedFrame.optimizations,
stringTable);
if (isLeaf) {
frameNode.youngestFrameSamples++;
}
frameNode.samples++;
frameNode._addOptimizations(inflatedFrame.optimizations, stringTable);
prevFrameKey = frameKey;
prevCalls = frameNode.calls;
isLeaf = mutableFrameKeyOptions.isLeaf = false;
}
this.duration += sampleDuration;
this.samples++;
prevSampleTime = sampleTime;
}
},
@ -347,7 +330,19 @@ ThreadNode.prototype = {
};
/**
* A function call node in a tree.
* A function call node in a tree. Represents a function call with a unique context,
* resulting in each FrameNode having its own row in the corresponding tree view.
* Take samples:
* A()->B()->C()
* A()->B()
* Q()->B()
*
* In inverted tree, A()->B()->C() would have one frame node, and A()->B() and
* Q()->B() would share a frame node.
* In an uninverted tree, A()->B()->C() and A()->B() would share a frame node,
* with Q()->B() having its own.
*
* In all cases, all the frame nodes originated from the same InflatedFrame.
*
* @param string frameKey
* The key associated with this frame. The key determines identity of
@ -372,8 +367,8 @@ function FrameNode(frameKey, { location, line, category, allocations, isContent
this.location = location;
this.line = line;
this.allocations = allocations;
this.youngestFrameSamples = 0;
this.samples = 0;
this.duration = 0;
this.calls = [];
this.isContent = !!isContent;
this._optimizations = null;
@ -384,22 +379,15 @@ function FrameNode(frameKey, { location, line, category, allocations, isContent
FrameNode.prototype = {
/**
* Count a sample as associated with this node.
* Take optimization data observed for this frame.
*
* @param number prevSampleTime
* The time when the immediate previous sample was sampled.
* @param number sampleTime
* The time when the current sample was sampled.
* @param object optimizationSite
* Any JIT optimization information attached to the current
* sample. Lazily inflated via stringTable.
* @param object stringTable
* The string table used to inflate the optimizationSite.
*/
_countSample: function (prevSampleTime, sampleTime, optimizationSite, stringTable) {
this.samples++;
this.duration += sampleTime - prevSampleTime;
_addOptimizations: function (optimizationSite, stringTable) {
// Simply accumulate optimization sites for now. Processing is done lazily
// by JITOptimizations, if optimization information is actually displayed.
if (optimizationSite) {
@ -424,7 +412,7 @@ FrameNode.prototype = {
}
this.samples += otherNode.samples;
this.duration += otherNode.duration;
this.youngestFrameSamples += otherNode.youngestFrameSamples;
if (otherNode._optimizations) {
let opts = this._optimizations;

View File

@ -28,7 +28,7 @@ const DEFAULT_SORTING_PREDICATE = (frameA, frameB) => {
}
return dataA.selfPercentage < dataB.selfPercentage ? 1 : - 1;
}
return dataA.samples < dataB.samples ? 1 : -1;
return dataA.totalPercentage < dataB.totalPercentage ? 1 : -1;
};
const DEFAULT_AUTO_EXPAND_DEPTH = 3; // depth
@ -358,54 +358,28 @@ CallView.prototype = Heritage.extend(AbstractTreeItem.prototype, {
// Leaf nodes in an inverted tree don't have to do anything special.
let isLeaf = this._level === 0;
let totalSamples = this.root.frame.samples;
let totalDuration = this.root.frame.duration;
if (this.inverted && !isLeaf && this.parent != null) {
let calleeData = this.parent.getDisplayedData();
// Percentage of time that this frame called the callee
// in this branch
let callerPercentage = this.frame.samples / calleeData.samples;
// Self duration, cost
if (this.visibleCells.selfDuration) {
data.selfDuration = this.frame.youngestFrameSamples / totalSamples * totalDuration;
}
if (this.visibleCells.selfPercentage) {
data.selfPercentage = this.frame.youngestFrameSamples / totalSamples * 100;
}
// Self/total duration.
if (this.visibleCells.duration) {
data.totalDuration = calleeData.totalDuration * callerPercentage;
}
if (this.visibleCells.selfDuration) {
data.selfDuration = 0;
}
// Total duration, cost
if (this.visibleCells.duration) {
data.totalDuration = this.frame.samples / totalSamples * totalDuration;
}
if (this.visibleCells.percentage) {
data.totalPercentage = this.frame.samples / totalSamples * 100;
}
// Self/total samples percentage.
if (this.visibleCells.percentage) {
data.totalPercentage = calleeData.totalPercentage * callerPercentage;
}
if (this.visibleCells.selfPercentage) {
data.selfPercentage = 0;
}
// Raw samples.
if (this.visibleCells.samples) {
data.samples = this.frame.samples;
}
} else {
// Self/total duration.
if (this.visibleCells.duration) {
data.totalDuration = this.frame.duration;
}
if (this.visibleCells.selfDuration) {
data.selfDuration = this.root.frame.selfDuration[this.frame.key];
}
// Self/total samples percentage.
if (this.visibleCells.percentage) {
data.totalPercentage = this.frame.samples / this.root.frame.samples * 100;
}
if (this.visibleCells.selfPercentage) {
data.selfPercentage = this.root.frame.selfCount[this.frame.key] / this.root.frame.samples * 100;
}
// Raw samples.
if (this.visibleCells.samples) {
data.samples = this.frame.samples;
}
// Raw samples.
if (this.visibleCells.samples) {
data.samples = this.frame.youngestFrameSamples;
}
// Self/total allocations count.

View File

@ -127,8 +127,6 @@ const EVENTS = {
// Emitted by the OverviewView when a range has been selected in the graphs
OVERVIEW_RANGE_SELECTED: "Performance:UI:OverviewRangeSelected",
// Emitted by the OverviewView when a selection range has been removed
OVERVIEW_RANGE_CLEARED: "Performance:UI:OverviewRangeCleared",
// Emitted by the DetailsView when a subview is selected
DETAILS_VIEW_SELECTED: "Performance:UI:DetailsViewSelected",

View File

@ -6,7 +6,7 @@
*/
function* spawnTest() {
let { panel } = yield initPerformance(SIMPLE_URL);
let { EVENTS, DetailsView, JsCallTreeView } = panel.panelWin;
let { EVENTS, DetailsView, WaterfallView, JsCallTreeView } = panel.panelWin;
yield startRecording(panel);
yield busyWait(100);
@ -19,11 +19,25 @@ function* spawnTest() {
ok(true, "JsCallTreeView rendered after recording is stopped.");
yield DetailsView.selectView("waterfall");
yield startRecording(panel);
yield busyWait(100);
let waterfallRenderedOnWF = once(WaterfallView, EVENTS.WATERFALL_RENDERED);
let waterfallRenderedOnJS = once(JsCallTreeView, EVENTS.WATERFALL_RENDERED);
rendered = once(JsCallTreeView, EVENTS.JS_CALL_TREE_RENDERED);
yield stopRecording(panel);
// The waterfall should render by default, and we want to make
// sure that the render events don't bleed between detail views
// via bug 1173393, so test that's the case after both views have been
// created
waterfallRenderedOnJS.then(() =>
ok(false, "JsCallTreeView should not receive WATERFALL_RENDERED event"));
yield waterfallRenderedOnWF;
yield DetailsView.selectView("js-calltree");
yield rendered;
ok(true, "JsCallTreeView rendered again after recording completed a second time.");

View File

@ -14,7 +14,7 @@ function* spawnTest() {
Services.prefs.setBoolPref(MEMORY_PREF, true);
let { panel } = yield initPerformance(SIMPLE_URL);
let { EVENTS, $, DetailsView, JsCallTreeView, MemoryCallTreeView } = panel.panelWin;
let { EVENTS, $, DetailsView, OverviewView, JsCallTreeView, MemoryCallTreeView } = panel.panelWin;
yield DetailsView.selectView("js-calltree");
ok(DetailsView.isViewSelected(JsCallTreeView), "The call tree is now selected.");
@ -26,7 +26,7 @@ function* spawnTest() {
yield rendered;
// Mock the profile used so we can get a deterministic tree created
let threadNode = new ThreadNode(gProfile.threads[0]);
let threadNode = new ThreadNode(gProfile.threads[0], OverviewView.getTimeInterval());
JsCallTreeView._populateCallTree(threadNode);
JsCallTreeView.emit(EVENTS.JS_CALL_TREE_RENDERED);

View File

@ -58,7 +58,7 @@ function* spawnTest() {
// Force a rerender
let rendered = once(JsCallTreeView, EVENTS.JS_CALL_TREE_RENDERED);
JsCallTreeView.render();
JsCallTreeView.render(OverviewView.getTimeInterval());
yield rendered;
is($("#jit-optimizations-view").hidden, true, "JIT Optimizations panel still hidden when rerendered");

View File

@ -61,7 +61,7 @@ function* spawnTest() {
// Force a rerender
let rendered = once(JsCallTreeView, EVENTS.JS_CALL_TREE_RENDERED);
JsCallTreeView.render();
JsCallTreeView.render(OverviewView.getTimeInterval());
yield rendered;
Services.prefs.setBoolPref(JIT_PREF, true);

View File

@ -7,7 +7,7 @@
function* spawnTest() {
let { panel } = yield initPerformance(SIMPLE_URL);
let { EVENTS, PerformanceController, OverviewView } = panel.panelWin;
let startTime, endTime, params, _;
let params, _;
yield startRecording(panel);
@ -21,32 +21,49 @@ function* spawnTest() {
let graph = OverviewView.graphs.get("timeline");
let MAX = graph.width;
let duration = PerformanceController.getCurrentRecording().getDuration();
let selection = null;
// Throw out events that select everything, as this will occur on the
// first click
OverviewView.on(EVENTS.OVERVIEW_RANGE_SELECTED, function handler (_, interval) {
if (interval.endTime !== duration) {
selection = interval;
OverviewView.off(handler);
}
});
// Select the first half of the graph
let results = onceSpread(OverviewView, EVENTS.OVERVIEW_RANGE_SELECTED);
// Select the first half of the graph
dragStart(graph, 0);
dragStop(graph, MAX / 2);
[_, { startTime, endTime }] = yield results;
yield waitUntil(() => selection);
let { startTime, endTime } = selection;
let mapStart = () => 0;
let mapEnd = () => PerformanceController.getCurrentRecording().getDuration();
let mapEnd = () => duration;
let actual = graph.getMappedSelection({ mapStart, mapEnd });
is(actual.min, 0, "graph selection starts at 0");
is(actual.max, duration/2, `graph selection ends at ${duration/2}`);
is(graph.hasSelection(), true,
"A selection exists on the graph.");
is(startTime, actual.min,
is(startTime, 0,
"OVERVIEW_RANGE_SELECTED fired with startTime value on click.");
is(endTime, actual.max,
is(endTime, duration / 2,
"OVERVIEW_RANGE_SELECTED fired with endTime value on click.");
// Listen to deselection
results = onceSpread(OverviewView, EVENTS.OVERVIEW_RANGE_CLEARED);
results = onceSpread(OverviewView, EVENTS.OVERVIEW_RANGE_SELECTED);
dropSelection(graph);
[_, params] = yield results;
is(graph.hasSelection(), false,
"A selection no longer on the graph.");
is(params, undefined,
"OVERVIEW_RANGE_CLEARED fired with no additional arguments.");
is(params.startTime, 0,
"OVERVIEW_RANGE_SELECTED fired with 0 as a startTime.");
is(params.endTime, duration,
"OVERVIEW_RANGE_SELECTED fired with max duration as endTime");
yield teardown(panel);
finish();

View File

@ -40,7 +40,7 @@ function* spawnTest() {
ok(true, "Flamegraph rerenders after its corresponding pane is shown.");
rendered = once(JsFlameGraphView, EVENTS.JS_FLAMEGRAPH_RENDERED);
OverviewView.emit(EVENTS.OVERVIEW_RANGE_CLEARED);
OverviewView.emit(EVENTS.OVERVIEW_RANGE_SELECTED);
yield rendered;
ok(true, "Flamegraph rerenders when a range in the overview graph is removed.");

View File

@ -10,7 +10,7 @@ function test() {
let { ThreadNode } = devtools.require("devtools/performance/tree-model");
let { CallView } = devtools.require("devtools/performance/tree-view");
let threadNode = new ThreadNode(gThread);
let threadNode = new ThreadNode(gThread, { startTime: 0, endTime: 20 });
// Don't display the synthesized (root) and the real (root) node twice.
threadNode.calls = threadNode.calls[0].calls;
let treeRoot = new CallView({ frame: threadNode });
@ -31,7 +31,7 @@ function test() {
is(container.childNodes[0].childNodes[0].getAttribute("type"), "duration",
"The root node in the tree has a duration cell.");
is(container.childNodes[0].childNodes[0].getAttribute("value"), "15 ms",
is(container.childNodes[0].childNodes[0].getAttribute("value"), "20 ms",
"The root node in the tree has the correct duration cell value.");
is(container.childNodes[0].childNodes[1].getAttribute("type"), "percentage",
@ -51,7 +51,7 @@ function test() {
is(container.childNodes[0].childNodes[4].getAttribute("type"), "samples",
"The root node in the tree has an samples cell.");
is(container.childNodes[0].childNodes[4].getAttribute("value"), "4",
is(container.childNodes[0].childNodes[4].getAttribute("value"), "0",
"The root node in the tree has the correct samples cell value.");
is(container.childNodes[0].childNodes[5].getAttribute("type"), "function",

View File

@ -12,7 +12,7 @@ function test() {
let { ThreadNode } = devtools.require("devtools/performance/tree-model");
let { CallView } = devtools.require("devtools/performance/tree-view");
let threadNode = new ThreadNode(gThread);
let threadNode = new ThreadNode(gThread, { startTime: 0, endTime: 20 });
// Don't display the synthesized (root) and the real (root) node twice.
threadNode.calls = threadNode.calls[0].calls;
let treeRoot = new CallView({ frame: threadNode });
@ -33,11 +33,11 @@ function test() {
is(container.childNodes[0].className, "call-tree-item",
"The root node in the tree has the correct class name.");
is($$dur(0).getAttribute("value"), "15 ms",
is($$dur(0).getAttribute("value"), "20 ms",
"The root's duration cell displays the correct value.");
is($$perc(0).getAttribute("value"), "100%",
"The root's percentage cell displays the correct value.");
is($$sampl(0).getAttribute("value"), "4",
is($$sampl(0).getAttribute("value"), "0",
"The root's samples cell displays the correct value.");
is($$fun(".call-tree-name")[0].getAttribute("value"), "(root)",
"The root's function cell displays the correct name.");
@ -59,11 +59,11 @@ function test() {
is(container.childNodes[1].className, "call-tree-item",
"The .A node in the tree has the correct class name.");
is($$dur(1).getAttribute("value"), "15 ms",
is($$dur(1).getAttribute("value"), "20 ms",
"The .A node's duration cell displays the correct value.");
is($$perc(1).getAttribute("value"), "100%",
"The .A node's percentage cell displays the correct value.");
is($$sampl(1).getAttribute("value"), "4",
is($$sampl(1).getAttribute("value"), "0",
"The .A node's samples cell displays the correct value.");
is($fun(".call-tree-name", $$(".call-tree-item")[1]).getAttribute("value"), "A",
"The .A node's function cell displays the correct name.");
@ -88,11 +88,11 @@ function test() {
is(container.childNodes[3].className, "call-tree-item",
"The .E node in the tree has the correct class name.");
is($$dur(2).getAttribute("value"), "8 ms",
is($$dur(2).getAttribute("value"), "15 ms",
"The .A.B node's duration cell displays the correct value.");
is($$perc(2).getAttribute("value"), "75%",
"The .A.B node's percentage cell displays the correct value.");
is($$sampl(2).getAttribute("value"), "3",
is($$sampl(2).getAttribute("value"), "0",
"The .A.B node's samples cell displays the correct value.");
is($fun(".call-tree-name", $$(".call-tree-item")[2]).getAttribute("value"), "B",
"The .A.B node's function cell displays the correct name.");
@ -107,11 +107,11 @@ function test() {
is($fun(".call-tree-category", $$(".call-tree-item")[2]).getAttribute("value"), "Styles",
"The .A.B node's function cell displays the correct category.");
is($$dur(3).getAttribute("value"), "7 ms",
is($$dur(3).getAttribute("value"), "5 ms",
"The .A.E node's duration cell displays the correct value.");
is($$perc(3).getAttribute("value"), "25%",
"The .A.E node's percentage cell displays the correct value.");
is($$sampl(3).getAttribute("value"), "1",
is($$sampl(3).getAttribute("value"), "0",
"The .A.E node's samples cell displays the correct value.");
is($fun(".call-tree-name", $$(".call-tree-item")[3]).getAttribute("value"), "E",
"The .A.E node's function cell displays the correct name.");

View File

@ -10,7 +10,7 @@ function test() {
let { ThreadNode } = devtools.require("devtools/performance/tree-model");
let { CallView } = devtools.require("devtools/performance/tree-view");
let threadNode = new ThreadNode(gThread);
let threadNode = new ThreadNode(gThread, { startTime: 0, endTime: 20 });
// Don't display the synthesized (root) and the real (root) node twice.
threadNode.calls = threadNode.calls[0].calls;
let treeRoot = new CallView({ frame: threadNode });
@ -57,19 +57,19 @@ function test() {
is($$name(6).getAttribute("value"), "F",
"The .A.E.F node's function cell displays the correct name.");
is($$duration(0).getAttribute("value"), "15 ms",
is($$duration(0).getAttribute("value"), "20 ms",
"The root node's function cell displays the correct duration.");
is($$duration(1).getAttribute("value"), "15 ms",
is($$duration(1).getAttribute("value"), "20 ms",
"The .A node's function cell displays the correct duration.");
is($$duration(2).getAttribute("value"), "8 ms",
is($$duration(2).getAttribute("value"), "15 ms",
"The .A.B node's function cell displays the correct duration.");
is($$duration(3).getAttribute("value"), "3 ms",
is($$duration(3).getAttribute("value"), "10 ms",
"The .A.B.D node's function cell displays the correct duration.");
is($$duration(4).getAttribute("value"), "5 ms",
"The .A.B.C node's function cell displays the correct duration.");
is($$duration(5).getAttribute("value"), "7 ms",
is($$duration(5).getAttribute("value"), "5 ms",
"The .A.E node's function cell displays the correct duration.");
is($$duration(6).getAttribute("value"), "7 ms",
is($$duration(6).getAttribute("value"), "5 ms",
"The .A.E.F node's function cell displays the correct duration.");
finish();

View File

@ -12,7 +12,7 @@ function test() {
let { ThreadNode } = devtools.require("devtools/performance/tree-model");
let { CallView } = devtools.require("devtools/performance/tree-view");
let threadNode = new ThreadNode(gThread);
let threadNode = new ThreadNode(gThread, { startTime: 0, endTime: 20 });
// Don't display the synthesized (root) and the real (root) node twice.
threadNode.calls = threadNode.calls[0].calls;
let treeRoot = new CallView({ frame: threadNode });

View File

@ -10,7 +10,7 @@ function test() {
let { ThreadNode } = devtools.require("devtools/performance/tree-model");
let { CallView } = devtools.require("devtools/performance/tree-view");
let threadNode = new ThreadNode(gThread);
let threadNode = new ThreadNode(gThread, { startTime: 0, endTime: 20 });
// Don't display the synthesized (root) and the real (root) node twice.
threadNode.calls = threadNode.calls[0].calls;
let treeRoot = new CallView({ frame: threadNode });

View File

@ -10,7 +10,7 @@ function* spawnTest() {
let { ThreadNode } = devtools.require("devtools/performance/tree-model");
let { CallView } = devtools.require("devtools/performance/tree-view");
let threadNode = new ThreadNode(gThread);
let threadNode = new ThreadNode(gThread, { startTime: 0, endTime: 20 });
// Don't display the synthesized (root) and the real (root) node twice.
threadNode.calls = threadNode.calls[0].calls;
let treeRoot = new CallView({ frame: threadNode });

View File

@ -10,7 +10,7 @@ function* spawnTest() {
let { ThreadNode } = devtools.require("devtools/performance/tree-model");
let { CallView } = devtools.require("devtools/performance/tree-view");
let threadNode = new ThreadNode(gThread);
let threadNode = new ThreadNode(gThread, { startTime: 0, endTime: 20 });
// Don't display the synthesized (root) and the real (root) node twice.
threadNode.calls = threadNode.calls[0].calls;
let treeRoot = new CallView({ frame: threadNode });

View File

@ -26,7 +26,7 @@ function test() {
* - (JS)
*/
let threadNode = new ThreadNode(gThread, { contentOnly: true });
let threadNode = new ThreadNode(gThread, { startTime: 0, endTime: 30, contentOnly: true });
// Don't display the synthesized (root) and the real (root) node twice.
threadNode.calls = threadNode.calls[0].calls;
let treeRoot = new CallView({ frame: threadNode, autoExpandDepth: 10 });

View File

@ -12,7 +12,7 @@ let test = Task.async(function*() {
let { ThreadNode } = devtools.require("devtools/performance/tree-model");
let { CallView } = devtools.require("devtools/performance/tree-view");
let threadNode = new ThreadNode(gSamples, { invertTree: true });
let threadNode = new ThreadNode(gSamples, { invertTree: true, startTime: 0, endTime: 10 });
let treeRoot = new CallView({ frame: threadNode, inverted: true, autoExpandDepth: 1 });
let container = document.createElement("vbox");

View File

@ -10,7 +10,7 @@ function test() {
let { ThreadNode } = devtools.require("devtools/performance/tree-model");
let { CallView } = devtools.require("devtools/performance/tree-view");
let threadNode = new ThreadNode(gThread, { invertTree: true });
let threadNode = new ThreadNode(gThread, { invertTree: true, startTime: 0, endTime: 50 });
let treeRoot = new CallView({ frame: threadNode, inverted: true, hidden: true });
let container = document.createElement("vbox");

View File

@ -14,7 +14,7 @@ add_task(function test() {
// Create a root node from a given samples array.
let threadNode = new ThreadNode(gThread);
let threadNode = new ThreadNode(gThread, { startTime: 0, endTime: 20 });
let root = getFrameNodePath(threadNode, "(root)");
// Test the root node.
@ -22,8 +22,8 @@ add_task(function test() {
equal(threadNode.getInfo().nodeType, "Thread",
"The correct node type was retrieved for the root node.");
equal(root.duration, 20,
"The correct duration was calculated for the root node.");
equal(threadNode.duration, 20,
"The correct duration was calculated for the ThreadNode.");
equal(root.getInfo().functionName, "(root)",
"The correct function name was retrieved for the root node.");
equal(root.getInfo().categoryData.abbrev, "other",
@ -82,32 +82,38 @@ add_task(function test() {
equal(getFrameNodePath(root, "A > E > F").calls.length, 0,
"The correct number of child calls were calculated for the 'A > E > F' node.");
// Check the location, sample times, duration and samples of the root.
// Check the location, sample times, and samples of the root.
equal(getFrameNodePath(root, "A").location, "A",
"The 'A' node has the correct location.");
equal(getFrameNodePath(root, "A").duration, 20,
"The 'A' node has the correct duration in milliseconds.");
equal(getFrameNodePath(root, "A").youngestFrameSamples, 0,
"The 'A' has correct `youngestFrameSamples`");
equal(getFrameNodePath(root, "A").samples, 4,
"The 'A' node has the correct number of samples.");
"The 'A' has correct `samples`");
// A frame that is both a leaf and caught in another stack
equal(getFrameNodePath(root, "A > B > C").youngestFrameSamples, 1,
"The 'A > B > C' has correct `youngestFrameSamples`");
equal(getFrameNodePath(root, "A > B > C").samples, 2,
"The 'A > B > C' has correct `samples`");
// ...and the rightmost leaf.
equal(getFrameNodePath(root, "A > E > F").location, "F",
"The 'A > E > F' node has the correct location.");
equal(getFrameNodePath(root, "A > E > F").duration, 7,
"The 'A > E > F' node has the correct duration in milliseconds.");
equal(getFrameNodePath(root, "A > E > F").samples, 1,
"The 'A > E > F' node has the correct number of samples.");
equal(getFrameNodePath(root, "A > E > F").youngestFrameSamples, 1,
"The 'A > E > F' node has the correct number of youngestFrameSamples.");
// ...and the leftmost leaf.
equal(getFrameNodePath(root, "A > B > C > D > E > F > G").location, "G",
"The 'A > B > C > D > E > F > G' node has the correct location.");
equal(getFrameNodePath(root, "A > B > C > D > E > F > G").duration, 2,
"The 'A > B > C > D > E > F > G' node has the correct duration in milliseconds.");
equal(getFrameNodePath(root, "A > B > C > D > E > F > G").samples, 1,
"The 'A > B > C > D > E > F > G' node has the correct number of samples.");
equal(getFrameNodePath(root, "A > B > C > D > E > F > G").youngestFrameSamples, 1,
"The 'A > B > C > D > E > F > G' node has the correct number of youngestFrameSamples.");
});
let gThread = synthesizeProfileForTest([{

View File

@ -14,12 +14,12 @@ add_task(function test() {
// Create a root node from a given samples array.
let root = getFrameNodePath(new ThreadNode(gThread), "(root)");
let thread = new ThreadNode(gThread, { startTime: 0, endTime: 10 });
let root = getFrameNodePath(thread, "(root)");
// Test the root node.
equal(root.duration, 5,
"The correct duration was calculated for the root node.");
// Test the ThreadNode, only node with a duration.
equal(thread.duration, 10,
"The correct duration was calculated for the ThreadNode.");
equal(root.calls.length, 1,
"The correct number of child calls were calculated for the root node.");

View File

@ -20,12 +20,13 @@ add_task(function test() {
// exactly at 18.
let startTime = 5;
let endTime = 18;
let root = getFrameNodePath(new ThreadNode(gThread, { startTime, endTime }), "(root)");
let thread = new ThreadNode(gThread, { startTime, endTime });
let root = getFrameNodePath(thread, "(root)");
// Test the root node.
equal(root.duration, endTime - startTime,
"The correct duration was calculated for the root node.");
equal(thread.duration, endTime - startTime,
"The correct duration was calculated for the ThreadNode.");
equal(root.calls.length, 1,
"The correct number of child calls were calculated for the root node.");

View File

@ -17,12 +17,12 @@ add_task(function test() {
let startTime = 5;
let endTime = 18;
let root = getFrameNodePath(new ThreadNode(gThread, { startTime: startTime, endTime: endTime, contentOnly: true }), "(root)");
let thread = new ThreadNode(gThread, { startTime, endTime, contentOnly: true });
let root = getFrameNodePath(thread, "(root)");
// Test the root node.
equal(root.duration, endTime - startTime,
"The correct duration was calculated for the root node.");
// Test the ThreadNode, only node which should have duration
equal(thread.duration, endTime - startTime,
"The correct duration was calculated for the root ThreadNode.");
equal(root.calls.length, 2,
"The correct number of child calls were calculated for the root node.");

View File

@ -49,7 +49,7 @@ function run_test() {
add_task(function test() {
let { ThreadNode } = devtools.require("devtools/performance/tree-model");
let root = new ThreadNode(gThread, { invertTree: true });
let root = new ThreadNode(gThread, { invertTree: true, startTime: 0, endTime: 4 });
equal(root.calls.length, 2,
"Should get the 2 youngest frames, not the 1 oldest frame");

View File

@ -159,7 +159,7 @@ function run_test() {
add_task(function test() {
let { ThreadNode } = devtools.require("devtools/performance/tree-model");
let root = new ThreadNode(gThread);
let root = new ThreadNode(gThread, { startTime: 0, endTime: 4 });
let A = getFrameNodePath(root, "(root) > A");

View File

@ -17,7 +17,7 @@ add_task(function test() {
// Create a root node from a given samples array.
let root = getFrameNodePath(new ThreadNode(gThread, { contentOnly: true }), "(root)");
let root = getFrameNodePath(new ThreadNode(gThread, { startTime: 5, endTime: 30, contentOnly: true }), "(root)");
/*
* should have a tree like:

View File

@ -17,7 +17,7 @@ add_task(function test() {
// Create a root node from a given samples array.
let root = getFrameNodePath(new ThreadNode(gThread, { contentOnly: true }), "(root)");
let root = getFrameNodePath(new ThreadNode(gThread, { startTime: 5, endTime: 25, contentOnly: true }), "(root)");
/*
* should have a tree like:

View File

@ -20,7 +20,6 @@ let DetailsSubview = {
PerformanceController.on(EVENTS.RECORDING_SELECTED, this._onRecordingStoppedOrSelected);
PerformanceController.on(EVENTS.PREF_CHANGED, this._onPrefChanged);
OverviewView.on(EVENTS.OVERVIEW_RANGE_SELECTED, this._onOverviewRangeChange);
OverviewView.on(EVENTS.OVERVIEW_RANGE_CLEARED, this._onOverviewRangeChange);
DetailsView.on(EVENTS.DETAILS_VIEW_SELECTED, this._onDetailsViewSelected);
},
@ -34,7 +33,6 @@ let DetailsSubview = {
PerformanceController.off(EVENTS.RECORDING_SELECTED, this._onRecordingStoppedOrSelected);
PerformanceController.off(EVENTS.PREF_CHANGED, this._onPrefChanged);
OverviewView.off(EVENTS.OVERVIEW_RANGE_SELECTED, this._onOverviewRangeChange);
OverviewView.off(EVENTS.OVERVIEW_RANGE_CLEARED, this._onOverviewRangeChange);
DetailsView.off(EVENTS.DETAILS_VIEW_SELECTED, this._onDetailsViewSelected);
},
@ -92,7 +90,7 @@ let DetailsSubview = {
return;
}
if (DetailsView.isViewSelected(this) || this.canUpdateWhileHidden) {
this.render();
this.render(OverviewView.getTimeInterval());
} else {
this.shouldUpdateWhenShown = true;
}
@ -160,8 +158,3 @@ let DetailsSubview = {
}
}
};
/**
* Convenient way of emitting events from the view.
*/
EventEmitter.decorate(DetailsSubview);

View File

@ -131,3 +131,5 @@ let JsCallTreeView = Heritage.extend(DetailsSubview, {
toString: () => "[object JsCallTreeView]"
});
EventEmitter.decorate(JsCallTreeView);

View File

@ -115,3 +115,5 @@ let JsFlameGraphView = Heritage.extend(DetailsSubview, {
toString: () => "[object JsFlameGraphView]"
});
EventEmitter.decorate(JsFlameGraphView);

View File

@ -118,3 +118,5 @@ let MemoryCallTreeView = Heritage.extend(DetailsSubview, {
toString: () => "[object MemoryCallTreeView]"
});
EventEmitter.decorate(MemoryCallTreeView);

View File

@ -113,3 +113,5 @@ let MemoryFlameGraphView = Heritage.extend(DetailsSubview, {
toString: () => "[object MemoryFlameGraphView]"
});
EventEmitter.decorate(MemoryFlameGraphView);

View File

@ -184,3 +184,5 @@ let WaterfallView = Heritage.extend(DetailsSubview, {
toString: () => "[object WaterfallView]"
});
EventEmitter.decorate(WaterfallView);

View File

@ -165,8 +165,10 @@ let OverviewView = {
let mapEnd = () => recording.getDuration();
let selection = this.graphs.getMappedSelection({ mapStart, mapEnd });
// If no selection returned, this means the overview graphs have not been rendered
// yet, so act as if we have no selection (the full recording).
if (!selection) {
// yet, so act as if we have no selection (the full recording). Also
// if the selection range distance is tiny, assume the range was cleared or just
// clicked, and we do not have a range.
if (!selection || (selection.max - selection.min) < 1) {
return { startTime: 0, endTime: recording.getDuration() };
}
return { startTime: selection.min, endTime: selection.max };
@ -300,14 +302,8 @@ let OverviewView = {
if (this._stopSelectionChangeEventPropagation) {
return;
}
// If the range is smaller than a pixel (which can happen when performing
// a click on the graphs), treat this as a cleared selection.
let interval = this.getTimeInterval();
if (interval.endTime - interval.startTime < 1) {
this.emit(EVENTS.OVERVIEW_RANGE_CLEARED);
} else {
this.emit(EVENTS.OVERVIEW_RANGE_SELECTED, interval);
}
this.emit(EVENTS.OVERVIEW_RANGE_SELECTED, this.getTimeInterval());
},
_onGraphRendered: function (_, graphName) {

View File

@ -280,6 +280,28 @@ Telemetry.prototype = {
}
},
/**
* Log a value to a keyed histogram.
*
* @param {String} histogramId
* Histogram in which the data is to be stored.
* @param {String} key
* The key within the single histogram.
* @param value
* Value to store.
*/
logKeyed: function(histogramId, key, value) {
if (histogramId) {
try {
let histogram = Services.telemetry.getKeyedHistogramById(histogramId);
histogram.add(key, value);
} catch(e) {
dump("Warning: An attempt was made to write to the " + histogramId +
" histogram, which is not defined in Histograms.json\n");
}
}
},
/**
* Log info about usage once per browser version. This allows us to discover
* how many individual users are using our tools for each browser version.

View File

@ -958,6 +958,21 @@ Rule.prototype = {
} else {
aTextProperty.rule.editor.closeBrace.focus();
}
},
/**
* Return a string representation of the rule.
*/
stringifyRule: function() {
let selectorText = this.selectorText;
let cssText = "";
let terminator = osString == "WINNT" ? "\r\n" : "\n";
for (let textProp of this.textProps) {
cssText += "\t" + textProp.stringifyProperty() + terminator;
}
return selectorText + " {" + terminator + cssText + "}";
}
};
@ -1083,6 +1098,21 @@ TextProperty.prototype = {
remove: function() {
this.rule.removeProperty(this);
},
/**
* Return a string representation of the rule property.
*/
stringifyProperty: function() {
// Get the displayed property value
let declaration = this.name + ": " + this.editor.committed.value + ";";
// Comment out property declarations that are not enabled
if (!this.enabled) {
declaration = "/* " + declaration + " */";
}
return declaration;
}
};
@ -1136,6 +1166,12 @@ function CssRuleView(aInspector, aDoc, aStore, aPageStyle) {
this._onCopy = this._onCopy.bind(this);
this._onCopyColor = this._onCopyColor.bind(this);
this._onCopyImageDataUrl = this._onCopyImageDataUrl.bind(this);
this._onCopyLocation = this._onCopyLocation.bind(this);
this._onCopyPropertyDeclaration = this._onCopyPropertyDeclaration.bind(this);
this._onCopyPropertyName = this._onCopyPropertyName.bind(this);
this._onCopyPropertyValue = this._onCopyPropertyValue.bind(this);
this._onCopyRule = this._onCopyRule.bind(this);
this._onCopySelector = this._onCopySelector.bind(this);
this._onToggleOrigSources = this._onToggleOrigSources.bind(this);
this._onShowMdnDocs = this._onShowMdnDocs.bind(this);
this._onFilterStyles = this._onFilterStyles.bind(this);
@ -1223,31 +1259,70 @@ CssRuleView.prototype = {
this._contextmenu.addEventListener("popupshowing", this._contextMenuUpdate);
this._contextmenu.id = "rule-view-context-menu";
this.menuitemAddRule = createMenuItem(this._contextmenu, {
label: "ruleView.contextmenu.addNewRule",
accesskey: "ruleView.contextmenu.addNewRule.accessKey",
command: this._onAddRule
});
this.menuitemSelectAll = createMenuItem(this._contextmenu, {
label: "ruleView.contextmenu.selectAll",
accesskey: "ruleView.contextmenu.selectAll.accessKey",
command: this._onSelectAll
});
this.menuitemCopy = createMenuItem(this._contextmenu, {
label: "ruleView.contextmenu.copy",
accesskey: "ruleView.contextmenu.copy.accessKey",
command: this._onCopy
});
this.menuitemCopyLocation = createMenuItem(this._contextmenu, {
label: "ruleView.contextmenu.copyLocation",
command: this._onCopyLocation
});
this.menuitemCopyRule = createMenuItem(this._contextmenu, {
label: "ruleView.contextmenu.copyRule",
command: this._onCopyRule
});
this.menuitemCopyColor = createMenuItem(this._contextmenu, {
label: "ruleView.contextmenu.copyColor",
accesskey: "ruleView.contextmenu.copyColor.accessKey",
command: this._onCopyColor
});
this.menuitemCopyImageDataUrl = createMenuItem(this._contextmenu, {
label: "styleinspector.contextmenu.copyImageDataUrl",
accesskey: "styleinspector.contextmenu.copyImageDataUrl.accessKey",
command: this._onCopyImageDataUrl
});
this.menuitemCopyPropertyDeclaration = createMenuItem(this._contextmenu, {
label: "ruleView.contextmenu.copyPropertyDeclaration",
command: this._onCopyPropertyDeclaration
});
this.menuitemCopyPropertyName = createMenuItem(this._contextmenu, {
label: "ruleView.contextmenu.copyPropertyName",
command: this._onCopyPropertyName
});
this.menuitemCopyPropertyValue = createMenuItem(this._contextmenu, {
label: "ruleView.contextmenu.copyPropertyValue",
command: this._onCopyPropertyValue
});
this.menuitemCopySelector = createMenuItem(this._contextmenu, {
label: "ruleView.contextmenu.copySelector",
command: this._onCopySelector
});
createMenuSeparator(this._contextmenu);
this.menuitemSelectAll = createMenuItem(this._contextmenu, {
label: "ruleView.contextmenu.selectAll",
accesskey: "ruleView.contextmenu.selectAll.accessKey",
command: this._onSelectAll
});
createMenuSeparator(this._contextmenu);
this.menuitemAddRule = createMenuItem(this._contextmenu, {
label: "ruleView.contextmenu.addNewRule",
accesskey: "ruleView.contextmenu.addNewRule.accessKey",
command: this._onAddRule
});
this.menuitemSources = createMenuItem(this._contextmenu, {
label: "ruleView.contextmenu.showOrigSources",
accesskey: "ruleView.contextmenu.showOrigSources.accessKey",
@ -1360,6 +1435,23 @@ CssRuleView.prototype = {
* appropriate.
*/
_contextMenuUpdate: function() {
this._enableCopyMenuItems(this.doc.popupNode.parentNode);
this.menuitemAddRule.disabled = this.inspector.selection.isAnonymousNode();
this.menuitemShowMdnDocs.hidden = !this.enableMdnDocsTooltip ||
!this.doc.popupNode.parentNode
.classList.contains(PROPERTY_NAME_CLASS);
let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
this.menuitemSources.setAttribute("checked", showOrig);
},
/**
* Display the necessary copy context menu items depending on the clicked
* node and selection in the rule view.
*/
_enableCopyMenuItems: function(target) {
let win = this.doc.defaultView;
// Copy selection.
@ -1382,18 +1474,31 @@ CssRuleView.prototype = {
copy = false;
}
this.menuitemCopy.hidden = !copy;
this.menuitemCopyColor.hidden = !this._isColorPopup();
this.menuitemCopyImageDataUrl.hidden = !this._isImageUrlPopup();
this.menuitemCopy.disabled = !copy;
let showOrig = Services.prefs.getBoolPref(PREF_ORIG_SOURCES);
this.menuitemSources.setAttribute("checked", showOrig);
this.menuitemCopyLocation.hidden = true;
this.menuitemCopyPropertyDeclaration.hidden = true;
this.menuitemCopyPropertyName.hidden = true;
this.menuitemCopyPropertyValue.hidden = true;
this.menuitemCopySelector.hidden = true;
this.menuitemShowMdnDocs.hidden = !this.enableMdnDocsTooltip ||
!this.doc.popupNode.parentNode
.classList.contains(PROPERTY_NAME_CLASS);
this._clickedNodeInfo = this.getNodeInfo(target);
this.menuitemAddRule.disabled = this.inspector.selection.isAnonymousNode();
if (!this._clickedNodeInfo) {
return;
} else if (this._clickedNodeInfo.type == overlays.VIEW_NODE_PROPERTY_TYPE) {
this.menuitemCopyPropertyDeclaration.hidden = false;
this.menuitemCopyPropertyName.hidden = false;
} else if (this._clickedNodeInfo.type == overlays.VIEW_NODE_VALUE_TYPE) {
this.menuitemCopyPropertyDeclaration.hidden = false;
this.menuitemCopyPropertyValue.hidden = false;
} else if (this._clickedNodeInfo.type == overlays.VIEW_NODE_SELECTOR_TYPE) {
this.menuitemCopySelector.hidden = false;
} else if (this._clickedNodeInfo.type == overlays.VIEW_NODE_LOCATION_TYPE) {
this.menuitemCopyLocation.hidden = false;
}
},
/**
@ -1422,7 +1527,8 @@ CssRuleView.prototype = {
enabled: prop.enabled,
overridden: prop.overridden,
pseudoElement: prop.rule.pseudoElement,
sheetHref: prop.rule.domRule.href
sheetHref: prop.rule.domRule.href,
textProperty: prop
};
} else if (classes.contains("ruleview-propertyvalue") && prop) {
type = overlays.VIEW_NODE_VALUE_TYPE;
@ -1432,7 +1538,8 @@ CssRuleView.prototype = {
enabled: prop.enabled,
overridden: prop.overridden,
pseudoElement: prop.rule.pseudoElement,
sheetHref: prop.rule.domRule.href
sheetHref: prop.rule.domRule.href,
textProperty: prop
};
} else if (classes.contains("theme-link") &&
!classes.contains("ruleview-rule-source") && prop) {
@ -1444,12 +1551,19 @@ CssRuleView.prototype = {
enabled: prop.enabled,
overridden: prop.overridden,
pseudoElement: prop.rule.pseudoElement,
sheetHref: prop.rule.domRule.href
sheetHref: prop.rule.domRule.href,
textProperty: prop
};
} else if (classes.contains("ruleview-selector-unmatched") ||
classes.contains("ruleview-selector-matched")) {
classes.contains("ruleview-selector-matched") ||
classes.contains("ruleview-selector")) {
type = overlays.VIEW_NODE_SELECTOR_TYPE;
value = node.textContent;
value = node.offsetParent._ruleEditor.selectorText.textContent;
} else if (classes.contains("ruleview-rule-source")) {
type = overlays.VIEW_NODE_LOCATION_TYPE;
let ruleEditor = node.offsetParent._ruleEditor;
let rule = ruleEditor.rule;
value = (rule.sheet && rule.sheet.href) ? rule.sheet.href : rule.title;
} else {
return null;
}
@ -1624,6 +1738,71 @@ CssRuleView.prototype = {
clipboardHelper.copyString(message);
}),
/**
* Copy the rule source location of the current clicked node.
*/
_onCopyLocation: function() {
if (!this._clickedNodeInfo) {
return;
}
clipboardHelper.copyString(this._clickedNodeInfo.value, this.doc);
},
/**
* Copy the rule property declaration of the current clicked node.
*/
_onCopyPropertyDeclaration: function() {
if (!this._clickedNodeInfo) {
return;
}
let textProp = this._clickedNodeInfo.value.textProperty;
clipboardHelper.copyString(textProp.stringifyProperty(), this.doc);
},
/**
* Copy the rule property name of the current clicked node.
*/
_onCopyPropertyName: function() {
if (!this._clickedNodeInfo) {
return;
}
clipboardHelper.copyString(this._clickedNodeInfo.value.property, this.doc);
},
/**
* Copy the rule property value of the current clicked node.
*/
_onCopyPropertyValue: function() {
if (!this._clickedNodeInfo) {
return;
}
clipboardHelper.copyString(this._clickedNodeInfo.value.value, this.doc);
},
/**
* Copy the rule of the current clicked node.
*/
_onCopyRule: function() {
let ruleEditor = this.doc.popupNode.parentNode.offsetParent._ruleEditor;
let rule = ruleEditor.rule;
clipboardHelper.copyString(rule.stringifyRule(), this.doc);
},
/**
* Copy the rule selector of the current clicked node.
*/
_onCopySelector: function() {
if (!this._clickedNodeInfo) {
return;
}
clipboardHelper.copyString(this._clickedNodeInfo.value, this.doc);
},
/**
* Toggle the original sources pref.
*/
@ -1831,16 +2010,43 @@ CssRuleView.prototype = {
this.menuitemCopyColor = null;
// Destroy Copy Data URI menuitem.
this.menuitemCopyImageDataUrl.removeEventListener("command", this._onCopyImageDataUrl);
this.menuitemCopyImageDataUrl.removeEventListener("command",
this._onCopyImageDataUrl);
this.menuitemCopyImageDataUrl = null;
this.menuitemCopyLocation.removeEventListener("command",
this._onCopyLocation);
this.menuitemCopyLocation = null;
this.menuitemCopyPropertyDeclaration.removeEventListener("command",
this._onCopyPropertyDeclaration);
this.menuitemCopyPropertyDeclaration = null;
this.menuitemCopyPropertyName.removeEventListener("command",
this._onCopyPropertyName);
this.menuitemCopyPropertyName = null;
this.menuitemCopyPropertyValue.removeEventListener("command",
this._onCopyPropertyValue);
this.menuitemCopyPropertyValue = null;
this.menuitemCopyRule.removeEventListener("command",
this._onCopyRule);
this.menuitemCopyRule = null;
this.menuitemCopySelector.removeEventListener("command",
this._onCopySelector);
this.menuitemCopySelector = null;
this.menuitemSources.removeEventListener("command",
this._onToggleOrigSources);
this._onToggleOrigSources);
this.menuitemSources = null;
this._clickedNodeInfo = null;
// Destroy the context menu.
this._contextmenu.removeEventListener("popupshowing",
this._contextMenuUpdate);
this._contextMenuUpdate);
this._contextmenu.parentNode.removeChild(this._contextmenu);
this._contextmenu = null;
}
@ -3628,8 +3834,12 @@ function createMenuItem(aMenu, aAttributes) {
let item = aMenu.ownerDocument.createElementNS(XUL_NS, "menuitem");
item.setAttribute("label", _strings.GetStringFromName(aAttributes.label));
item.setAttribute("accesskey",
_strings.GetStringFromName(aAttributes.accesskey));
if (aAttributes.accesskey) {
item.setAttribute("accesskey",
_strings.GetStringFromName(aAttributes.accesskey));
}
item.addEventListener("command", aAttributes.command);
if (aAttributes.type) {
@ -3641,6 +3851,11 @@ function createMenuItem(aMenu, aAttributes) {
return item;
}
function createMenuSeparator(aMenu) {
let separator = aMenu.ownerDocument.createElementNS(XUL_NS, "menuseparator");
aMenu.appendChild(separator);
}
function setTimeout() {
let window = Services.appShell.hiddenDOMWindow;
return window.setTimeout.apply(window, arguments);
@ -3795,6 +4010,10 @@ XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
.getService(Ci.nsIClipboardHelper);
});
XPCOMUtils.defineLazyGetter(this, "osString", function() {
return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
});
XPCOMUtils.defineLazyGetter(this, "_strings", function() {
return Services.strings.createBundle(
"chrome://global/locale/devtools/styleinspector.properties");

View File

@ -37,6 +37,7 @@ const VIEW_NODE_SELECTOR_TYPE = exports.VIEW_NODE_SELECTOR_TYPE = 1;
const VIEW_NODE_PROPERTY_TYPE = exports.VIEW_NODE_PROPERTY_TYPE = 2;
const VIEW_NODE_VALUE_TYPE = exports.VIEW_NODE_VALUE_TYPE = 3;
const VIEW_NODE_IMAGE_URL_TYPE = exports.VIEW_NODE_IMAGE_URL_TYPE = 4;
const VIEW_NODE_LOCATION_TYPE = exports.VIEW_NODE_LOCATION_TYPE = 5;
/**
* Manages all highlighters in the style-inspector.

View File

@ -9,6 +9,8 @@ support-files =
doc_content_stylesheet_linked.css
doc_content_stylesheet_script.css
doc_content_stylesheet_xul.css
doc_copystyles.css
doc_copystyles.html
doc_filter.html
doc_frame_script.js
doc_keyframeanimation.html
@ -76,6 +78,7 @@ skip-if = e10s # Bug 1039528: "inspect element" contextual-menu doesn't work wit
[browser_ruleview_context-menu-show-mdn-docs-01.js]
[browser_ruleview_context-menu-show-mdn-docs-02.js]
[browser_ruleview_context-menu-show-mdn-docs-03.js]
[browser_ruleview_copy_styles.js]
[browser_ruleview_cubicbezier-appears-on-swatch-click.js]
[browser_ruleview_cubicbezier-commit-on-ENTER.js]
[browser_ruleview_cubicbezier-revert-on-ESC.js]

View File

@ -0,0 +1,246 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
/**
* Tests the behaviour of the copy styles context menu items in the rule
* view
*/
XPCOMUtils.defineLazyGetter(this, "osString", function() {
return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).OS;
});
let TEST_URI = TEST_URL_ROOT + "doc_copystyles.html";
add_task(function*() {
yield addTab(TEST_URI);
let { inspector, view } = yield openRuleView();
yield selectNode("#testid", inspector);
let ruleEditor = getRuleViewRuleEditor(view, 1);
let data = [
{
desc: "Test Copy Property Name",
node: ruleEditor.rule.textProps[0].editor.nameSpan,
menuItem: view.menuitemCopyPropertyName,
expectedPattern: "color",
hidden: {
copyLocation: true,
copyPropertyDeclaration: false,
copyPropertyName: false,
copyPropertyValue: true,
copySelector: true,
copyRule: false
}
},
{
desc: "Test Copy Property Value",
node: ruleEditor.rule.textProps[2].editor.valueSpan,
menuItem: view.menuitemCopyPropertyValue,
expectedPattern: "12px",
hidden: {
copyLocation: true,
copyPropertyDeclaration: false,
copyPropertyName: true,
copyPropertyValue: false,
copySelector: true,
copyRule: false
}
},
{
desc: "Test Copy Property Declaration",
node: ruleEditor.rule.textProps[2].editor.nameSpan,
menuItem: view.menuitemCopyPropertyDeclaration,
expectedPattern: "font-size: 12px;",
hidden: {
copyLocation: true,
copyPropertyDeclaration: false,
copyPropertyName: false,
copyPropertyValue: true,
copySelector: true,
copyRule: false
}
},
{
desc: "Test Copy Rule",
node: ruleEditor.rule.textProps[2].editor.nameSpan,
menuItem: view.menuitemCopyRule,
expectedPattern: "#testid {[\\r\\n]+" +
"\tcolor: #F00;[\\r\\n]+" +
"\tbackground-color: #00F;[\\r\\n]+" +
"\tfont-size: 12px;[\\r\\n]+" +
"}",
hidden: {
copyLocation: true,
copyPropertyDeclaration: false,
copyPropertyName: false,
copyPropertyValue: true,
copySelector: true,
copyRule: false
}
},
{
desc: "Test Copy Selector",
node: ruleEditor.selectorText,
menuItem: view.menuitemCopySelector,
expectedPattern: "html, body, #testid",
hidden: {
copyLocation: true,
copyPropertyDeclaration: true,
copyPropertyName: true,
copyPropertyValue: true,
copySelector: false,
copyRule: false
}
},
{
desc: "Test Copy Location",
node: ruleEditor.source,
menuItem: view.menuitemCopyLocation,
expectedPattern: "http://example.com/browser/browser/devtools/" +
"styleinspector/test/doc_copystyles.css",
hidden: {
copyLocation: false,
copyPropertyDeclaration: true,
copyPropertyName: true,
copyPropertyValue: true,
copySelector: true,
copyRule: false
}
},
{
setup: function*() {
yield disableProperty(view);
},
desc: "Test Copy Rule with Disabled Property",
node: ruleEditor.rule.textProps[2].editor.nameSpan,
menuItem: view.menuitemCopyRule,
expectedPattern: "#testid {[\\r\\n]+" +
"\t\/\\* color: #F00; \\*\/[\\r\\n]+" +
"\tbackground-color: #00F;[\\r\\n]+" +
"\tfont-size: 12px;[\\r\\n]+" +
"}",
hidden: {
copyLocation: true,
copyPropertyDeclaration: false,
copyPropertyName: false,
copyPropertyValue: true,
copySelector: true,
copyRule: false
}
},
{
desc: "Test Copy Property Declaration with Disabled Property",
node: ruleEditor.rule.textProps[0].editor.nameSpan,
menuItem: view.menuitemCopyPropertyDeclaration,
expectedPattern: "\/\\* color: #F00; \\*\/",
hidden: {
copyLocation: true,
copyPropertyDeclaration: false,
copyPropertyName: false,
copyPropertyValue: true,
copySelector: true,
copyRule: false
}
},
];
for (let { setup, desc, node, menuItem, expectedPattern, hidden } of data) {
if (setup) {
yield setup();
}
info(desc);
yield checkCopyStyle(view, node, menuItem, expectedPattern, hidden);
}
});
function* checkCopyStyle(view, node, menuItem, expectedPattern, hidden) {
let win = view.doc.defaultView;
let onPopup = once(view._contextmenu, "popupshown");
EventUtils.synthesizeMouseAtCenter(node,
{button: 2, type: "contextmenu"}, win);
yield onPopup;
is(view.menuitemCopy.hidden, true, "Copy hidden is as expected: true");
is(view.menuitemCopyLocation.hidden,
hidden.copyLocation,
"Copy Location hidden attribute is as expected: " +
hidden.copyLocation);
is(view.menuitemCopyPropertyDeclaration.hidden,
hidden.copyPropertyDeclaration,
"Copy Property Declaration hidden attribute is as expected: " +
hidden.copyPropertyDeclaration);
is(view.menuitemCopyPropertyName.hidden,
hidden.copyPropertyName,
"Copy Property Name hidden attribute is as expected: " +
hidden.copyPropertyName);
is(view.menuitemCopyPropertyValue.hidden,
hidden.copyPropertyValue,
"Copy Property Value hidden attribute is as expected: " +
hidden.copyPropertyValue);
is(view.menuitemCopySelector.hidden,
hidden.copySelector,
"Copy Selector hidden attribute is as expected: " +
hidden.copySelector);
is(view.menuitemCopyRule.hidden,
hidden.copyRule,
"Copy Rule hidden attribute is as expected: " +
hidden.copyRule);
try {
yield waitForClipboard(() => menuItem.click(),
() => checkClipboardData(expectedPattern));
} catch(e) {
failedClipboard(expectedPattern);
}
view._contextmenu.hidePopup();
}
function* disableProperty(view) {
let ruleEditor = getRuleViewRuleEditor(view, 1);
let propEditor = ruleEditor.rule.textProps[0].editor;
info("Disabling a property");
propEditor.enable.click();
yield ruleEditor.rule._applyingModifications;
}
function checkClipboardData(expectedPattern) {
let actual = SpecialPowers.getClipboardData("text/unicode");
let expectedRegExp = new RegExp(expectedPattern, "g");
return expectedRegExp.test(actual);
}
function failedClipboard(expectedPattern) {
// Format expected text for comparison
let terminator = osString == "WINNT" ? "\r\n" : "\n";
expectedPattern = expectedPattern.replace(/\[\\r\\n\][+*]/g, terminator);
expectedPattern = expectedPattern.replace(/\\\(/g, "(");
expectedPattern = expectedPattern.replace(/\\\)/g, ")");
let actual = SpecialPowers.getClipboardData("text/unicode");
// Trim the right hand side of our strings. This is because expectedPattern
// accounts for windows sometimes adding a newline to our copied data.
expectedPattern = expectedPattern.trimRight();
actual = actual.trimRight();
ok(false, "Clipboard text does not match expected " +
"results (escaped for accurate comparison):\n");
info("Actual: " + escape(actual));
info("Expected: " + escape(expectedPattern));
}

View File

@ -33,8 +33,7 @@ add_task(function*() {
'</div>';
content.document.title = "Rule view context menu test";
info("Opening the computed view");
let {toolbox, inspector, view} = yield openRuleView();
let {inspector, view} = yield openRuleView();
info("Selecting the test node");
yield selectNode("div", inspector);
@ -43,17 +42,18 @@ add_task(function*() {
yield checkSelectAll(view);
});
function checkCopySelection(view) {
function* checkCopySelection(view) {
info("Testing selection copy");
let contentDoc = view.doc;
let win = contentDoc.defaultView;
let prop = contentDoc.querySelector(".ruleview-property");
let values = contentDoc.querySelectorAll(".ruleview-propertyvaluecontainer");
let range = contentDoc.createRange();
range.setStart(prop, 0);
range.setEnd(values[4], 2);
let selection = view.doc.defaultView.getSelection().addRange(range);
view.doc.defaultView.getSelection().addRange(range);
info("Checking that _Copy() returns the correct clipboard value");
@ -65,19 +65,28 @@ function checkCopySelection(view) {
"html {[\\r\\n]+" +
" color: #000;[\\r\\n]*";
return waitForClipboard(() => {
fireCopyEvent(prop);
}, () => {
return checkClipboardData(expectedPattern);
}).then(() => {}, () => {
let onPopup = once(view._contextmenu, "popupshown");
EventUtils.synthesizeMouseAtCenter(prop,
{button: 2, type: "contextmenu"}, win);
yield onPopup;
ok(!view.menuitemCopy.hidden, "Copy menu item is not hidden as expected");
try {
yield waitForClipboard(() => view.menuitemCopy.click(),
() => checkClipboardData(expectedPattern));
} catch(e) {
failedClipboard(expectedPattern);
});
}
view._contextmenu.hidePopup();
}
function checkSelectAll(view) {
function* checkSelectAll(view) {
info("Testing select-all copy");
let contentDoc = view.doc;
let win = contentDoc.defaultView;
let prop = contentDoc.querySelector(".ruleview-property");
info("Checking that _SelectAll() then copy returns the correct clipboard value");
@ -93,13 +102,21 @@ function checkSelectAll(view) {
" color: #000;[\\r\\n]+" +
"}[\\r\\n]*";
return waitForClipboard(() => {
fireCopyEvent(prop);
}, () => {
return checkClipboardData(expectedPattern);
}).then(() => {}, () => {
let onPopup = once(view._contextmenu, "popupshown");
EventUtils.synthesizeMouseAtCenter(prop,
{button: 2, type: "contextmenu"}, win);
yield onPopup;
ok(!view.menuitemCopy.hidden, "Copy menu item is not hidden as expected");
try {
yield waitForClipboard(() => view.menuitemCopy.click(),
() => checkClipboardData(expectedPattern));
} catch(e) {
failedClipboard(expectedPattern);
});
}
view._contextmenu.hidePopup();
}
function checkClipboardData(expectedPattern) {

View File

@ -0,0 +1,9 @@
/* 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/. */
html, body, #testid {
color: #F00;
background-color: #00F;
font-size: 12px;
}

View File

@ -0,0 +1,11 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<html>
<head>
<title>Test case for copying stylesheet in rule-view</title>
<link rel="stylesheet" type="text/css" href="doc_copystyles.css"/>
</head>
<body>
<div id='testid'>Styled Node</div>
</body>
</html>

View File

@ -129,6 +129,8 @@ let AppManager = exports.AppManager = {
* runtime-list:
* The list of available runtimes has changed, or any of the user-visible
* details (like names) for the non-selected runtimes has changed.
* runtime-telemetry:
* Detailed runtime telemetry has been recorded. Used by tests.
* runtime-targets:
* The list of remote runtime targets available from the currently
* connected runtime (such as tabs or apps) has changed, or any of the
@ -183,6 +185,7 @@ let AppManager = exports.AppManager = {
// first.
this._appsFront = front;
this._listTabsResponse = response;
this._recordRuntimeInfo();
this.update("runtime-global-actors");
})
.then(() => {
@ -192,6 +195,7 @@ let AppManager = exports.AppManager = {
});
} else {
this._listTabsResponse = response;
this._recordRuntimeInfo();
this.update("runtime-global-actors");
}
});
@ -498,6 +502,33 @@ let AppManager = exports.AppManager = {
return deferred.promise;
},
_recordRuntimeInfo: Task.async(function*() {
if (!this.connected) {
return;
}
let runtime = this.selectedRuntime;
this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE",
runtime.type || "UNKNOWN", true);
this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID",
runtime.id || "unknown", true);
if (!this.deviceFront) {
this.update("runtime-telemetry");
return;
}
let d = yield this.deviceFront.getDescription();
this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PROCESSOR",
d.processor, true);
this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_OS",
d.os, true);
this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PLATFORM_VERSION",
d.platformversion, true);
this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_APP_TYPE",
d.apptype, true);
this._telemetry.logKeyed("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_VERSION",
d.version, true);
this.update("runtime-telemetry");
}),
isMainProcessDebuggable: function() {
// Fx <39 exposes chrome tab actors on RootActor
// Fx >=39 exposes a dedicated actor via getProcess request

View File

@ -35,11 +35,18 @@
this.telemetryInfo[histogramId].push(value);
}
}
Telemetry.prototype._oldlogKeyed = Telemetry.prototype.logKeyed;
Telemetry.prototype.logKeyed = function(histogramId, key, value) {
// This simple reduction is enough to test WebIDE's usage
this.log(`${histogramId}|${key}`, value);
}
}
function resetTelemetry() {
Telemetry.prototype.log = Telemetry.prototype._oldlog;
Telemetry.prototype.logKeyed = Telemetry.prototype._oldlogKeyed;
delete Telemetry.prototype._oldlog;
delete Telemetry.prototype._oldlogKeyed;
delete Telemetry.prototype.telemetryInfo;
}
@ -76,7 +83,7 @@
};
win.AppManager.runtimeList.wifi.push(wifi);
let sim = new _SimulatorRuntime("fakeSimulator");
let sim = new _SimulatorRuntime({ id: "fakeSimulator" });
// Use local pipe instead
sim.connect = function(connection) {
ok(connection, win.AppManager.connection, "connection is valid");
@ -139,6 +146,10 @@
ok(win.document.querySelector("window").className, "busy", "UI is busy");
yield win.UI._busyPromise;
is(Object.keys(DebuggerServer._connections).length, 1, "Connected");
// Logging runtime info needs to use the device actor
yield waitForUpdate(win, "runtime-global-actors");
// Ensure detailed telemetry is recorded
yield waitForUpdate(win, "runtime-telemetry");
});
}
@ -198,6 +209,48 @@
});
ok(okay, "All " + histId + " actions were skipped");
} else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE|USB") {
is(value.length, 1, histId + " has 1 connection results");
} else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE|WIFI") {
is(value.length, 1, histId + " has 1 connection results");
} else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE|SIMULATOR") {
is(value.length, 1, histId + " has 1 connection results");
} else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE|REMOTE") {
is(value.length, 1, histId + " has 1 connection results");
} else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE|LOCAL") {
is(value.length, 1, histId + " has 1 connection results");
} else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE|OTHER") {
is(value.length, 1, histId + " has 1 connection results");
} else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID|fakeUSB") {
is(value.length, 1, histId + " has 1 connection results");
} else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID|fakeWiFi") {
is(value.length, 1, histId + " has 1 connection results");
} else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID|fakeSimulator") {
is(value.length, 1, histId + " has 1 connection results");
} else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID|unknown") {
is(value.length, 1, histId + " has 1 connection results");
} else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID|local") {
is(value.length, 2, histId + " has 2 connection results");
} else if (histId.startsWith("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PROCESSOR")) {
let processor = histId.split("|")[1];
is(processor, Services.appinfo.XPCOMABI.split("-")[0], "Found runtime processor");
is(value.length, 6, histId + " has 6 connection results");
} else if (histId.startsWith("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_OS")) {
let os = histId.split("|")[1];
is(os, Services.appinfo.OS, "Found runtime OS");
is(value.length, 6, histId + " has 6 connection results");
} else if (histId.startsWith("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PLATFORM_VERSION")) {
let platformversion = histId.split("|")[1];
is(platformversion, Services.appinfo.platformVersion, "Found runtime platform version");
is(value.length, 6, histId + " has 6 connection results");
} else if (histId.startsWith("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_APP_TYPE")) {
let apptype = histId.split("|")[1];
is(apptype, "firefox", "Found runtime app type");
is(value.length, 6, histId + " has 6 connection results");
} else if (histId.startsWith("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_VERSION")) {
let version = histId.split("|")[1];
is(version, Services.appinfo.version, "Found runtime version");
is(value.length, 6, histId + " has 6 connection results");
} else {
ok(false, "Unexpected " + histId + " was logged");
}
@ -205,12 +258,13 @@
}
window.onload = function() {
SimpleTest.testInChaosMode();
SimpleTest.waitForExplicitFinish();
let win;
SimpleTest.registerCleanupFunction(() => {
Task.spawn(function*() {
return Task.spawn(function*() {
if (win) {
yield closeWebIDE(win);
}
@ -245,18 +299,13 @@
// Each one should log a connection result and non-zero connection
// time
yield connectToRuntime(win, docRuntime, "usb");
yield waitForTime(TOOL_DELAY);
yield connectToRuntime(win, docRuntime, "wifi");
yield waitForTime(TOOL_DELAY);
yield connectToRuntime(win, docRuntime, "simulator");
yield waitForTime(TOOL_DELAY);
yield connectToRuntime(win, docRuntime, "other", 0 /* remote */);
yield waitForTime(TOOL_DELAY);
yield connectToRuntime(win, docRuntime, "other", 1 /* local */);
yield waitForTime(TOOL_DELAY);
yield connectToRuntime(win, docRuntime, "other", 2 /* other */);
yield waitForTime(TOOL_DELAY);
yield closeWebIDE(win);
win = null;
checkResults();

View File

@ -35,11 +35,18 @@
this.telemetryInfo[histogramId].push(value);
}
}
Telemetry.prototype._oldlogKeyed = Telemetry.prototype.logKeyed;
Telemetry.prototype.logKeyed = function(histogramId, key, value) {
// This simple reduction is enough to test WebIDE's usage
this.log(`${histogramId}|${key}`, value);
}
}
function resetTelemetry() {
Telemetry.prototype.log = Telemetry.prototype._oldlog;
Telemetry.prototype.logKeyed = Telemetry.prototype._oldlogKeyed;
delete Telemetry.prototype._oldlog;
delete Telemetry.prototype._oldlogKeyed;
delete Telemetry.prototype.telemetryInfo;
}
@ -76,7 +83,7 @@
};
win.AppManager.runtimeList.wifi.push(wifi);
let sim = new _SimulatorRuntime("fakeSimulator");
let sim = new _SimulatorRuntime({ id: "fakeSimulator" });
// Use local pipe instead
sim.connect = function(connection) {
ok(connection, win.AppManager.connection, "connection is valid");
@ -139,6 +146,10 @@
ok(win.document.querySelector("window").className, "busy", "UI is busy");
yield win.UI._busyPromise;
is(Object.keys(DebuggerServer._connections).length, 1, "Connected");
// Logging runtime info needs to use the device actor
yield waitForUpdate(win, "runtime-global-actors");
// Ensure detailed telemetry is recorded
yield waitForUpdate(win, "runtime-telemetry");
});
}
@ -198,6 +209,48 @@
});
ok(okay, "All " + histId + " actions were skipped");
} else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE|USB") {
is(value.length, 1, histId + " has 1 connection results");
} else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE|WIFI") {
is(value.length, 1, histId + " has 1 connection results");
} else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE|SIMULATOR") {
is(value.length, 1, histId + " has 1 connection results");
} else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE|REMOTE") {
is(value.length, 1, histId + " has 1 connection results");
} else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE|LOCAL") {
is(value.length, 1, histId + " has 1 connection results");
} else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_TYPE|OTHER") {
is(value.length, 1, histId + " has 1 connection results");
} else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID|fakeUSB") {
is(value.length, 1, histId + " has 1 connection results");
} else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID|fakeWiFi") {
is(value.length, 1, histId + " has 1 connection results");
} else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID|fakeSimulator") {
is(value.length, 1, histId + " has 1 connection results");
} else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID|unknown") {
is(value.length, 1, histId + " has 1 connection results");
} else if (histId === "DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_ID|local") {
is(value.length, 2, histId + " has 2 connection results");
} else if (histId.startsWith("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PROCESSOR")) {
let processor = histId.split("|")[1];
is(processor, Services.appinfo.XPCOMABI.split("-")[0], "Found runtime processor");
is(value.length, 6, histId + " has 6 connection results");
} else if (histId.startsWith("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_OS")) {
let os = histId.split("|")[1];
is(os, Services.appinfo.OS, "Found runtime OS");
is(value.length, 6, histId + " has 6 connection results");
} else if (histId.startsWith("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_PLATFORM_VERSION")) {
let platformversion = histId.split("|")[1];
is(platformversion, Services.appinfo.platformVersion, "Found runtime platform version");
is(value.length, 6, histId + " has 6 connection results");
} else if (histId.startsWith("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_APP_TYPE")) {
let apptype = histId.split("|")[1];
is(apptype, "firefox", "Found runtime app type");
is(value.length, 6, histId + " has 6 connection results");
} else if (histId.startsWith("DEVTOOLS_WEBIDE_CONNECTED_RUNTIME_VERSION")) {
let version = histId.split("|")[1];
is(version, Services.appinfo.version, "Found runtime version");
is(value.length, 6, histId + " has 6 connection results");
} else {
ok(false, "Unexpected " + histId + " was logged");
}
@ -205,12 +258,13 @@
}
window.onload = function() {
SimpleTest.testInChaosMode();
SimpleTest.waitForExplicitFinish();
let win;
SimpleTest.registerCleanupFunction(() => {
Task.spawn(function*() {
return Task.spawn(function*() {
if (win) {
yield closeWebIDE(win);
}
@ -242,18 +296,13 @@
// Each one should log a connection result and non-zero connection
// time
yield connectToRuntime(win, "usb");
yield waitForTime(TOOL_DELAY);
yield connectToRuntime(win, "wifi");
yield waitForTime(TOOL_DELAY);
yield connectToRuntime(win, "simulator");
yield waitForTime(TOOL_DELAY);
yield connectToRuntime(win, "other", 0 /* remote */);
yield waitForTime(TOOL_DELAY);
yield connectToRuntime(win, "other", 1 /* local */);
yield waitForTime(TOOL_DELAY);
yield connectToRuntime(win, "other", 2 /* other */);
yield waitForTime(TOOL_DELAY);
yield closeWebIDE(win);
win = null;
checkResults();

View File

@ -301,8 +301,7 @@ browser.jar:
* skin/classic/browser/devtools/ruleview.css (../shared/devtools/ruleview.css)
* skin/classic/browser/devtools/webconsole.css (devtools/webconsole.css)
skin/classic/browser/devtools/webconsole_networkpanel.css (devtools/webconsole_networkpanel.css)
skin/classic/browser/devtools/webconsole.png (../shared/devtools/images/webconsole.png)
skin/classic/browser/devtools/webconsole@2x.png (../shared/devtools/images/webconsole@2x.png)
skin/classic/browser/devtools/webconsole.svg (../shared/devtools/images/webconsole.svg)
skin/classic/browser/devtools/commandline.css (../shared/devtools/commandline.css)
skin/classic/browser/devtools/markup-view.css (../shared/devtools/markup-view.css)
skin/classic/browser/devtools/editor-error.png (../shared/devtools/images/editor-error.png)

View File

@ -419,8 +419,7 @@ browser.jar:
skin/classic/browser/devtools/editor-debug-location@2x.png (../shared/devtools/images/editor-debug-location@2x.png)
* skin/classic/browser/devtools/webconsole.css (devtools/webconsole.css)
skin/classic/browser/devtools/webconsole_networkpanel.css (devtools/webconsole_networkpanel.css)
skin/classic/browser/devtools/webconsole.png (../shared/devtools/images/webconsole.png)
skin/classic/browser/devtools/webconsole@2x.png (../shared/devtools/images/webconsole@2x.png)
skin/classic/browser/devtools/webconsole.svg (../shared/devtools/images/webconsole.svg)
skin/classic/browser/devtools/breadcrumbs-divider@2x.png (../shared/devtools/images/breadcrumbs-divider@2x.png)
skin/classic/browser/devtools/breadcrumbs-scrollbutton.png (../shared/devtools/images/breadcrumbs-scrollbutton.png)
skin/classic/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="72" height="60" viewBox="0 0 72 60">
<defs>
<rect id="glyphShape-colorSwatch" width="8" height="8" ry="2" rx="2"/>
<rect id="glyphShape-colorSwatch-border" width="10" height="10" ry="2" rx="2"/>
<polygon id="glyphShape-errorX" points="9.9,8.5 8.5,9.9 6,7.4 3.6,9.8 2.2,8.4 4.6,6 2.2,3.6 3.6,2.2 6,4.6 8.4,2.2 9.8,3.6 7.4,6"/>
<path id="glyphShape-warningTriangle" d="M9.9,8.6l-3.1-6C6.6,2.2,6.3,2,6,2C5.7,2,5.4,2.2,5.2,2.5l-3.1,6C2,8.9,2,9.3,2.1,9.6C2.3,9.8,2.6,10,2.9,10 h6.1c0.4,0,0.6-0.2,0.8-0.4C10,9.3,10,8.9,9.9,8.6z"/>
<path id="glyphShape-exclamationPoint" d="M6,7.7c-0.6,0-1,0.4-1,0.8C5,9,5.4,9.3,6,9.3c0.6,0,1-0.4,1-0.8 C7,8.1,6.6,7.7,6,7.7z M6,7c0.6,0,1-0.4,1-1V5c0-0.6-0.4-1-1-1S5,4.4,5,5v1C5,6.6,5.4,7,6,7z"/>
<circle id="glyphShape-infoCircle" cx="6" cy="6" r="4"/>
<path id="glyphShape-infoGlyph" d="M6,6C5.4,6,5,6.4,5,7v1c0,0.6,0.4,1,1,1s1-0.4,1-1V7C7,6.4,6.6,6,6,6z M6,5c0.6,0,1-0.4,1-1S6.6,3,6,3S5,3.4,5,4S5.4,5,6,5z"/>
<style>
.icon-colorSwatch-border {
fill: #fff;
fill-opacity: .7;
}
.icon-colorSwatch-network {
fill: #000;
}
.icon-colorSwatch-css {
fill: #00b6f0;
}
.icon-colorSwatch-js {
fill: #fb9500;
}
.icon-colorSwatch-logging {
fill: #808080;
}
.icon-colorSwatch-security {
fill: #ec1e0d;
}
.icon-glyphOverlay {
fill: #fff;
}
#icon-indicator-input {
fill: #8fa1b2;
}
#icon-indicator-output {
fill: #667380;
}
#light-icons:target #icon-indicator-input {
fill: #45494d;
}
#light-icons:target #icon-indicator-output {
fill: #8a9199;
}
</style>
</defs>
<g id="icon-colorSwatch-network">
<use xlink:href="#glyphShape-colorSwatch-border" class="icon-colorSwatch-border" x="1" y="1"/>
<use xlink:href="#glyphShape-colorSwatch" class="icon-colorSwatch-network" x="2" y="2"/>
</g>
<g id="icon-colorSwatch-css" transform="translate(0 12)">
<use xlink:href="#glyphShape-colorSwatch-border" class="icon-colorSwatch-border" x="1" y="1"/>
<use xlink:href="#glyphShape-colorSwatch" class="icon-colorSwatch-css" x="2" y="2"/>
</g>
<g id="icon-colorSwatch-js" transform="translate(0 24)">
<use xlink:href="#glyphShape-colorSwatch-border" class="icon-colorSwatch-border" x="1" y="1"/>
<use xlink:href="#glyphShape-colorSwatch" class="icon-colorSwatch-js" x="2" y="2"/>
</g>
<g id="icon-colorSwatch-logging" transform="translate(0 36)">
<use xlink:href="#glyphShape-colorSwatch-border" class="icon-colorSwatch-border" x="1" y="1"/>
<use xlink:href="#glyphShape-colorSwatch" class="icon-colorSwatch-logging" x="2" y="2"/>
</g>
<g id="icon-colorSwatch-security" transform="translate(0 48)">
<use xlink:href="#glyphShape-colorSwatch-border" class="icon-colorSwatch-border" x="1" y="1"/>
<use xlink:href="#glyphShape-colorSwatch" class="icon-colorSwatch-security" x="2" y="2"/>
</g>
<use xlink:href="#glyphShape-errorX" id="icon-errorX-network" class="icon-colorSwatch-network" transform="translate(12)"/>
<use xlink:href="#glyphShape-errorX" id="icon-errorX-css" class="icon-colorSwatch-css" transform="translate(12 12)"/>
<use xlink:href="#glyphShape-errorX" id="icon-errorX-js" class="icon-colorSwatch-js" transform="translate(12 24)"/>
<use xlink:href="#glyphShape-errorX" id="icon-errorX-logging" class="icon-colorSwatch-logging" transform="translate(12 36)"/>
<use xlink:href="#glyphShape-errorX" id="icon-errorX-security" class="icon-colorSwatch-security" transform="translate(12 48)"/>
<g id="icon-warningTriangle-css" transform="translate(24 12)">
<use xlink:href="#glyphShape-warningTriangle" class="icon-colorSwatch-css"/>
<use xlink:href="#glyphShape-exclamationPoint" class="icon-glyphOverlay"/>
</g>
<g id="icon-warningTriangle-js" transform="translate(24 24)">
<use xlink:href="#glyphShape-warningTriangle" class="icon-colorSwatch-js"/>
<use xlink:href="#glyphShape-exclamationPoint" class="icon-glyphOverlay"/>
</g>
<g id="icon-warningTriangle-logging" transform="translate(24 36)">
<use xlink:href="#glyphShape-warningTriangle" class="icon-colorSwatch-logging"/>
<use xlink:href="#glyphShape-exclamationPoint" class="icon-glyphOverlay"/>
</g>
<g id="icon-warningTriangle-security" transform="translate(24 48)">
<use xlink:href="#glyphShape-warningTriangle" class="icon-colorSwatch-security"/>
<use xlink:href="#glyphShape-exclamationPoint" class="icon-glyphOverlay"/>
</g>
<g id="icon-infoCircle-logging" transform="translate(36 36)">
<use xlink:href="#glyphShape-infoCircle" class="icon-colorSwatch-logging"/>
<use xlink:href="#glyphShape-infoGlyph" class="icon-glyphOverlay"/>
</g>
<g id="light-icons">
<path id="icon-indicator-input" d="M6.5,1.2L5.4,2.3L9,6L5.3,9.7l1.1,1.1L11,6L6.5,1.2z M1.5,1.2 L0.4,2.3L4,6L0.3,9.7l1.1,1.1L6,6L1.5,1.2z" transform="translate(48 36)"/>
<polygon id="icon-indicator-output" points="10,5 4.3,5 6.8,2.4 5.5,1.2 1,6 5.5,10.8 6.9,9.6 4.3,7 10,7" transform="translate(60 36)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -639,25 +639,24 @@ menuitem.marker-color-graphs-blue:before,
#jit-optimizations-view .opt-icon::before {
content: "";
background-image: url(chrome://browser/skin/devtools/webconsole.png);
background-image: url(chrome://browser/skin/devtools/webconsole.svg);
background-repeat: no-repeat;
background-size: 48px 40px;
margin: 5px 6px 0 0;
width: 8px;
height: 8px;
max-height: 8px;
background-size: 72px 60px;
width: 12px;
height: 12px;
display: inline-block;
margin: 5px 6px 0 0;
max-height: 12px;
}
.theme-light #jit-optimizations-view .opt-icon::before {
background-image: url(chrome://browser/skin/devtools/webconsole.svg#light-icons);
}
#jit-optimizations-view .opt-icon[severity=warning]::before {
background-position: -16px -16px;
background-position: -24px -24px;
}
@media (min-resolution: 1.25dppx) {
#jit-optimizations-view .opt-icon::before {
background-image: url(chrome://browser/skin/devtools/webconsole@2x.png);
}
}
/**
* Configurable Options
@ -671,21 +670,21 @@ menuitem.marker-color-graphs-blue:before,
*/
menuitem.experimental-option::before {
content: "";
background-image: url(chrome://browser/skin/devtools/webconsole.png);
background-image: url(chrome://browser/skin/devtools/webconsole.svg);
background-repeat: no-repeat;
background-size: 48px 40px;
margin: 2px 5px 0 0;
width: 8px;
height: 8px;
max-height: 8px;
background-size: 72px 60px;
width: 12px;
height: 12px;
display: inline-block;
background-position: -16px -16px;
background-position: -24px -24px;
margin: 2px 5px 0 0;
max-height: 12px;
}
@media (min-resolution: 1.25dppx) {
menuitem.experimental-option::before {
background-image: url(chrome://browser/skin/devtools/webconsole@2x.png);
}
.theme-light menuitem.experimental-option::before {
background-image: url(chrome://browser/skin/devtools/webconsole.svg#light-icons);
}
#performance-options-menupopup:not(.experimental-enabled) .experimental-option,
#performance-options-menupopup:not(.experimental-enabled) .experimental-option::before {
display: none;

View File

@ -44,19 +44,17 @@ a {
.message > .icon::before {
content: "";
background-image: url(chrome://browser/skin/devtools/webconsole.png);
background-position: 8px 8px;
background-image: url(chrome://browser/skin/devtools/webconsole.svg);
background-position: 12px 12px;
background-repeat: no-repeat;
background-size: 48px 40px;
width: 8px;
height: 8px;
background-size: 72px 60px;
width: 12px;
height: 12px;
display: inline-block;
}
@media (min-resolution: 1.25dppx) {
.message > .icon::before {
background-image: url(chrome://browser/skin/devtools/webconsole@2x.png);
}
.theme-light .message > .icon::before {
background-image: url(chrome://browser/skin/devtools/webconsole.svg#light-icons);
}
.message > .message-body-wrapper {
@ -228,7 +226,7 @@ a {
}
.message[category=network][severity=error] > .icon::before {
background-position: -8px 0;
background-position: -12px 0;
}
.message[category=network] > .message-body {
@ -285,11 +283,11 @@ a {
}
.message[category=cssparser][severity=error] > .icon::before {
background-position: -8px -8px;
background-position: -12px -12px;
}
.message[category=cssparser][severity=warn] > .icon::before {
background-position: -16px -8px;
background-position: -24px -12px;
}
/* JS styles */
@ -303,11 +301,11 @@ a {
}
.message[category=exception][severity=error] > .icon::before {
background-position: -8px -16px;
background-position: -12px -24px;
}
.message[category=exception][severity=warn] > .icon::before {
background-position: -16px -16px;
background-position: -24px -24px;
}
/* Web Developer styles */
@ -322,15 +320,15 @@ a {
.message[category=console][severity=error] > .icon::before,
.message[category=output][severity=error] > .icon::before {
background-position: -8px -24px;
background-position: -12px -36px;
}
.message[category=console][severity=warn] > .icon::before {
background-position: -16px -24px;
background-position: -24px -36px;
}
.message[category=console][severity=info] > .icon::before {
background-position: -24px -24px;
background-position: -36px -36px;
}
/* Input and output styles */
@ -340,11 +338,11 @@ a {
}
.message[category=input] > .icon::before {
background-position: -32px -24px;
background-position: -48px -36px;
}
.message[category=output] > .icon::before {
background-position: -40px -24px;
background-position: -60px -36px;
}
/* JSTerm Styles */
@ -411,11 +409,11 @@ a {
}
.message[category=security][severity=error] > .icon::before {
background-position: -8px -32px;
background-position: -12px -48px;
}
.message[category=security][severity=warn] > .icon::before {
background-position: -16px -32px;
background-position: -24px -48px;
}
.navigation-marker {

View File

@ -400,8 +400,7 @@ browser.jar:
skin/classic/browser/devtools/editor-debug-location@2x.png (../shared/devtools/images/editor-debug-location@2x.png)
* skin/classic/browser/devtools/webconsole.css (devtools/webconsole.css)
skin/classic/browser/devtools/webconsole_networkpanel.css (devtools/webconsole_networkpanel.css)
skin/classic/browser/devtools/webconsole.png (../shared/devtools/images/webconsole.png)
skin/classic/browser/devtools/webconsole@2x.png (../shared/devtools/images/webconsole@2x.png)
skin/classic/browser/devtools/webconsole.svg (../shared/devtools/images/webconsole.svg)
skin/classic/browser/devtools/breadcrumbs-divider@2x.png (../shared/devtools/images/breadcrumbs-divider@2x.png)
skin/classic/browser/devtools/breadcrumbs-scrollbutton.png (../shared/devtools/images/breadcrumbs-scrollbutton.png)
skin/classic/browser/devtools/breadcrumbs-scrollbutton@2x.png (../shared/devtools/images/breadcrumbs-scrollbutton@2x.png)

View File

@ -4952,6 +4952,13 @@ if test -n "$MOZ_ANDROID_SHARE_OVERLAY"; then
AC_DEFINE(MOZ_ANDROID_SHARE_OVERLAY)
fi
dnl ========================================================
dnl = Show Firefox Account profile details on Android
dnl ========================================================
if test -n "$MOZ_ANDROID_FIREFOX_ACCOUNT_PROFILES"; then
AC_DEFINE(MOZ_ANDROID_FIREFOX_ACCOUNT_PROFILES)
fi
dnl = Include Tab Queue on Android
dnl = Temporary build flag to allow development in Nightly
dnl ========================================================
@ -8540,6 +8547,7 @@ AC_SUBST(MOZ_ANDROID_GECKOLIBS_AAR)
AC_SUBST(MOZ_ANDROID_READING_LIST_SERVICE)
AC_SUBST(MOZ_ANDROID_SEARCH_ACTIVITY)
AC_SUBST(MOZ_ANDROID_SHARE_OVERLAY)
AC_SUBST(MOZ_ANDROID_FIREFOX_ACCOUNT_PROFILES)
AC_SUBST(MOZ_ANDROID_TAB_QUEUE)
AC_SUBST(MOZ_ANDROID_MLS_STUMBLER)
AC_SUBST(MOZ_ANDROID_DOWNLOADS_INTEGRATION)

View File

@ -19,7 +19,7 @@ public class AboutPages {
public static final String FIREFOX = "about:firefox";
public static final String HEALTHREPORT = "about:healthreport";
public static final String HOME = "about:home";
public static final String PASSWORDS = "about:passwords";
public static final String LOGINS = "about:logins";
public static final String PRIVATEBROWSING = "about:privatebrowsing";
public static final String READER = "about:reader";
public static final String UPDATER = "about:";

View File

@ -175,6 +175,13 @@ public class AppConstants {
false;
//#endif
public static final boolean MOZ_ANDROID_FIREFOX_ACCOUNT_PROFILES =
//#ifdef MOZ_ANDROID_FIREFOX_ACCOUNT_PROFILES
true;
//#else
false;
//#endif
public static final boolean MOZ_TELEMETRY_ON_BY_DEFAULT =
//#ifdef MOZ_TELEMETRY_ON_BY_DEFAULT
true;

View File

@ -6,7 +6,6 @@
package org.mozilla.gecko;
import com.nineoldandroids.animation.Animator;
import com.nineoldandroids.animation.AnimatorListenerAdapter;
import com.nineoldandroids.animation.ObjectAnimator;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.DynamicToolbar.PinReason;
@ -37,6 +36,7 @@ import org.mozilla.gecko.health.HealthRecorder;
import org.mozilla.gecko.health.SessionInformation;
import org.mozilla.gecko.home.BrowserSearch;
import org.mozilla.gecko.home.HomeBanner;
import org.mozilla.gecko.home.HomeConfig.PanelType;
import org.mozilla.gecko.home.HomePager;
import org.mozilla.gecko.home.HomePager.OnUrlOpenInBackgroundListener;
import org.mozilla.gecko.home.HomePager.OnUrlOpenListener;
@ -157,6 +157,7 @@ public class BrowserApp extends GeckoApp
BrowserSearch.OnEditSuggestionListener,
OnUrlOpenListener,
OnUrlOpenInBackgroundListener,
ReadingListHelper.OnReadingListEventListener,
AnchoredPopup.OnVisibilityChangeListener,
ActionModeCompat.Presenter,
LayoutInflater.Factory {
@ -496,6 +497,12 @@ public class BrowserApp extends GeckoApp
case BOOKMARK_REMOVED:
showBookmarkRemovedToast();
break;
case READING_LIST_ADDED:
onAddedToReadingList(tab.getURL());
break;
case READING_LIST_REMOVED:
onRemovedFromReadingList(tab.getURL());
break;
case UNSELECTED:
// We receive UNSELECTED immediately after the SELECTED listeners run
@ -574,6 +581,36 @@ public class BrowserApp extends GeckoApp
Toast.makeText(this, R.string.bookmark_removed, Toast.LENGTH_SHORT).show();
}
private void showSwitchToReadingListToast(String message) {
getButtonToast().show(false,
message,
ButtonToast.LENGTH_SHORT,
getResources().getString(R.string.switch_button_message),
R.drawable.switch_button_icon,
new ButtonToast.ToastListener() {
@Override
public void onButtonClicked() {
final String aboutPageUrl = AboutPages.getURLForBuiltinPanelType(PanelType.READING_LIST);
Tabs.getInstance().loadUrlInTab(aboutPageUrl);
}
@Override
public void onToastHidden(ButtonToast.ReasonHidden reason) { }
});
}
public void onAddedToReadingList(String url) {
showSwitchToReadingListToast(getResources().getString(R.string.reading_list_added));
}
public void onAlreadyInReadingList(String url) {
showSwitchToReadingListToast(getResources().getString(R.string.reading_list_duplicate));
}
public void onRemovedFromReadingList(String url) {
Toast.makeText(this, R.string.reading_list_removed, Toast.LENGTH_SHORT).show();
}
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (AndroidGamepadManager.handleKeyEvent(event)) {
@ -808,7 +845,7 @@ public class BrowserApp extends GeckoApp
mSharedPreferencesHelper = new SharedPreferencesHelper(appContext);
mOrderedBroadcastHelper = new OrderedBroadcastHelper(appContext);
mBrowserHealthReporter = new BrowserHealthReporter();
mReadingListHelper = new ReadingListHelper(appContext, getProfile());
mReadingListHelper = new ReadingListHelper(appContext, getProfile(), this);
if (AppConstants.MOZ_ANDROID_BEAM) {
NfcAdapter nfc = NfcAdapter.getDefaultAdapter(this);
@ -3385,7 +3422,7 @@ public class BrowserApp extends GeckoApp
}
if (itemId == R.id.logins) {
Tabs.getInstance().loadUrlInTab(AboutPages.PASSWORDS);
Tabs.getInstance().loadUrlInTab(AboutPages.LOGINS);
return true;
}

View File

@ -8,7 +8,6 @@ import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.db.BrowserContract.ReadingListItems;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.DBUtils;
import org.mozilla.gecko.db.ReadingListAccessor;
import org.mozilla.gecko.favicons.Favicons;
import org.mozilla.gecko.mozglue.RobocopTarget;
@ -23,21 +22,32 @@ import android.content.ContentValues;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
import android.widget.Toast;
public final class ReadingListHelper implements NativeEventListener {
private static final String LOGTAG = "GeckoReadingListHelper";
public interface OnReadingListEventListener {
void onAddedToReadingList(String url);
void onRemovedFromReadingList(String url);
void onAlreadyInReadingList(String url);
}
private enum ReadingListEvent {
ADDED,
REMOVED,
ALREADY_EXISTS
}
protected final Context context;
private final BrowserDB db;
private final ReadingListAccessor readingListAccessor;
private final ContentObserver contentObserver;
private final OnReadingListEventListener onReadingListEventListener;
volatile boolean fetchInBackground = true;
public ReadingListHelper(Context context, GeckoProfile profile) {
public ReadingListHelper(Context context, GeckoProfile profile, OnReadingListEventListener listener) {
this.context = context;
this.db = profile.getDB();
this.readingListAccessor = db.getReadingListAccessor();
@ -56,6 +66,8 @@ public final class ReadingListHelper implements NativeEventListener {
};
this.readingListAccessor.registerContentObserver(context, contentObserver);
onReadingListEventListener = listener;
}
public void uninit() {
@ -110,11 +122,11 @@ public final class ReadingListHelper implements NativeEventListener {
@Override
public void run() {
if (readingListAccessor.isReadingListItem(cr, url)) {
showToast(R.string.reading_list_duplicate, Toast.LENGTH_SHORT);
handleEvent(ReadingListEvent.ALREADY_EXISTS, url);
callback.sendError("URL already in reading list: " + url);
} else {
readingListAccessor.addReadingListItem(cr, values);
showToast(R.string.reading_list_added, Toast.LENGTH_SHORT);
handleEvent(ReadingListEvent.ADDED, url);
callback.sendSuccess(url);
}
}
@ -227,7 +239,7 @@ public final class ReadingListHelper implements NativeEventListener {
@Override
public void run() {
readingListAccessor.removeReadingListItemWithURL(context.getContentResolver(), url);
showToast(R.string.reading_list_removed, Toast.LENGTH_SHORT);
handleEvent(ReadingListEvent.REMOVED, url);
}
});
}
@ -257,13 +269,23 @@ public final class ReadingListHelper implements NativeEventListener {
}
/**
* Show various status toasts.
* Handle various reading list events (and display appropriate toasts).
*/
private void showToast(final int resId, final int duration) {
private void handleEvent(final ReadingListEvent event, final String url) {
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(context, resId, duration).show();
switch(event) {
case ADDED:
onReadingListEventListener.onAddedToReadingList(url);
break;
case REMOVED:
onReadingListEventListener.onRemovedFromReadingList(url);
break;
case ALREADY_EXISTS:
onReadingListEventListener.onAlreadyInReadingList(url);
break;
}
}
});
}

View File

@ -582,12 +582,7 @@ public class Tab {
}
mDB.getReadingListAccessor().addBasicReadingListItem(getContentResolver(), url, mTitle);
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(mAppContext, R.string.reading_list_added, Toast.LENGTH_SHORT).show();
}
});
Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.READING_LIST_ADDED);
}
});
}
@ -604,12 +599,7 @@ public class Tab {
url = ReaderModeUtils.getUrlFromAboutReader(url);
}
mDB.getReadingListAccessor().removeReadingListItemWithURL(getContentResolver(), url);
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(mAppContext, R.string.reading_list_removed, Toast.LENGTH_SHORT).show();
}
});
Tabs.getInstance().notifyListeners(Tab.this, Tabs.TabEvents.READING_LIST_REMOVED);
}
});
}

View File

@ -638,7 +638,9 @@ public class Tabs implements GeckoEventListener {
VIEWPORT_CHANGE,
RECORDING_CHANGE,
BOOKMARK_ADDED,
BOOKMARK_REMOVED
BOOKMARK_REMOVED,
READING_LIST_ADDED,
READING_LIST_REMOVED,
}
public void notifyListeners(Tab tab, TabEvents msg) {

View File

@ -299,6 +299,7 @@ public class BrowserContract {
public static final class Clients {
private Clients() {}
public static final Uri CONTENT_RECENCY_URI = Uri.withAppendedPath(TABS_AUTHORITY_URI, "clients_recency");
public static final Uri CONTENT_URI = Uri.withAppendedPath(TABS_AUTHORITY_URI, "clients");
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/client";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/client";

View File

@ -33,10 +33,19 @@ public class LocalTabsAccessor implements TabsAccessor {
BrowserContract.Tabs.URL,
BrowserContract.Clients.GUID,
BrowserContract.Clients.NAME,
BrowserContract.Tabs.LAST_USED,
BrowserContract.Clients.LAST_MODIFIED,
BrowserContract.Clients.DEVICE_TYPE,
};
public static final String[] CLIENTS_PROJECTION_COLUMNS = new String[] {
BrowserContract.Clients.GUID,
BrowserContract.Clients.NAME,
BrowserContract.Clients.LAST_MODIFIED,
BrowserContract.Clients.DEVICE_TYPE
};
private static final String REMOTE_CLIENTS_SELECTION = BrowserContract.Clients.GUID + " IS NOT NULL";
private static final String LOCAL_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NULL";
private static final String REMOTE_TABS_SELECTION = BrowserContract.Tabs.CLIENT_GUID + " IS NOT NULL";
@ -53,12 +62,49 @@ public class LocalTabsAccessor implements TabsAccessor {
private static final Pattern FILTERED_URL_PATTERN = Pattern.compile("^(about|chrome|wyciwyg|file):");
private final Uri clientsRecencyUriWithProfile;
private final Uri tabsUriWithProfile;
private final Uri clientsUriWithProfile;
public LocalTabsAccessor(String mProfile) {
tabsUriWithProfile = DBUtils.appendProfileWithDefault(mProfile, BrowserContract.Tabs.CONTENT_URI);
clientsUriWithProfile = DBUtils.appendProfileWithDefault(mProfile, BrowserContract.Clients.CONTENT_URI);
public LocalTabsAccessor(String profileName) {
tabsUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Tabs.CONTENT_URI);
clientsUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Clients.CONTENT_URI);
clientsRecencyUriWithProfile = DBUtils.appendProfileWithDefault(profileName, BrowserContract.Clients.CONTENT_RECENCY_URI);
}
/**
* Extracts a List of just RemoteClients from a cursor.
* The supplied cursor should be grouped by guid and sorted by most recently used.
*/
@Override
public List<RemoteClient> getClientsWithoutTabsByRecencyFromCursor(Cursor cursor) {
final ArrayList<RemoteClient> clients = new ArrayList<>(cursor.getCount());
final int originalPosition = cursor.getPosition();
try {
if (!cursor.moveToFirst()) {
return clients;
}
final int clientGuidIndex = cursor.getColumnIndex(BrowserContract.Clients.GUID);
final int clientNameIndex = cursor.getColumnIndex(BrowserContract.Clients.NAME);
final int clientLastModifiedIndex = cursor.getColumnIndex(BrowserContract.Clients.LAST_MODIFIED);
final int clientDeviceTypeIndex = cursor.getColumnIndex(BrowserContract.Clients.DEVICE_TYPE);
while (!cursor.isAfterLast()) {
final String clientGuid = cursor.getString(clientGuidIndex);
final String clientName = cursor.getString(clientNameIndex);
final String deviceType = cursor.getString(clientDeviceTypeIndex);
final long lastModified = cursor.getLong(clientLastModifiedIndex);
clients.add(new RemoteClient(clientGuid, clientName, lastModified, deviceType));
cursor.moveToNext();
}
} finally {
cursor.moveToPosition(originalPosition);
}
return clients;
}
/**
@ -85,6 +131,7 @@ public class LocalTabsAccessor implements TabsAccessor {
final int tabTitleIndex = cursor.getColumnIndex(BrowserContract.Tabs.TITLE);
final int tabUrlIndex = cursor.getColumnIndex(BrowserContract.Tabs.URL);
final int tabLastUsedIndex = cursor.getColumnIndex(BrowserContract.Tabs.LAST_USED);
final int clientGuidIndex = cursor.getColumnIndex(BrowserContract.Clients.GUID);
final int clientNameIndex = cursor.getColumnIndex(BrowserContract.Clients.NAME);
final int clientLastModifiedIndex = cursor.getColumnIndex(BrowserContract.Clients.LAST_MODIFIED);
@ -106,7 +153,8 @@ public class LocalTabsAccessor implements TabsAccessor {
final String tabTitle = cursor.getString(tabTitleIndex);
final String tabUrl = cursor.getString(tabUrlIndex);
lastClient.tabs.add(new RemoteTab(tabTitle, tabUrl));
final long tabLastUsed = cursor.getLong(tabLastUsedIndex);
lastClient.tabs.add(new RemoteTab(tabTitle, tabUrl, tabLastUsed));
cursor.moveToNext();
}
@ -117,6 +165,13 @@ public class LocalTabsAccessor implements TabsAccessor {
return clients;
}
@Override
public Cursor getRemoteClientsByRecencyCursor(Context context) {
final Uri uri = clientsRecencyUriWithProfile;
return context.getContentResolver().query(uri, CLIENTS_PROJECTION_COLUMNS,
REMOTE_CLIENTS_SELECTION, null, null);
}
@Override
public Cursor getRemoteTabsCursor(Context context) {
return getRemoteTabsCursor(context, -1);

View File

@ -19,10 +19,12 @@ import android.os.Parcelable;
public class RemoteTab implements Parcelable {
public final String title;
public final String url;
public final long lastUsed;
public RemoteTab(String title, String url) {
public RemoteTab(String title, String url, long lastUsed) {
this.title = title;
this.url = url;
this.lastUsed = lastUsed;
}
@Override
@ -34,6 +36,7 @@ public class RemoteTab implements Parcelable {
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeString(title);
parcel.writeString(url);
parcel.writeLong(lastUsed);
}
public static final Creator<RemoteTab> CREATOR = new Creator<RemoteTab>() {
@ -41,8 +44,8 @@ public class RemoteTab implements Parcelable {
public RemoteTab createFromParcel(final Parcel source) {
final String title = source.readString();
final String url = source.readString();
return new RemoteTab(title, url);
final long lastUsed = source.readLong();
return new RemoteTab(title, url, lastUsed);
}
@Override

View File

@ -113,11 +113,21 @@ class StubTabsAccessor implements TabsAccessor {
public StubTabsAccessor() {
}
@Override
public List<RemoteClient> getClientsWithoutTabsByRecencyFromCursor(Cursor cursor) {
return new ArrayList<>();
}
@Override
public List<RemoteClient> getClientsFromCursor(final Cursor cursor) {
return new ArrayList<RemoteClient>();
}
@Override
public Cursor getRemoteClientsByRecencyCursor(Context context) {
return null;
}
public Cursor getRemoteTabsCursor(Context context) {
return null;
}

View File

@ -17,8 +17,10 @@ public interface TabsAccessor {
public void onQueryTabsComplete(List<RemoteClient> clients);
}
public Cursor getRemoteClientsByRecencyCursor(Context context);
public Cursor getRemoteTabsCursor(Context context);
public Cursor getRemoteTabsCursor(Context context, int limit);
public List<RemoteClient> getClientsWithoutTabsByRecencyFromCursor(final Cursor cursor);
public List<RemoteClient> getClientsFromCursor(final Cursor cursor);
public void getTabs(final Context context, final OnQueryTabsCompleteListener listener);
public void getTabs(final Context context, final int limit, final OnQueryTabsCompleteListener listener);

View File

@ -35,9 +35,11 @@ public class TabsProvider extends PerProfileDatabaseProvider<TabsProvider.TabsDa
static final int TABS_ID = 601;
static final int CLIENTS = 602;
static final int CLIENTS_ID = 603;
static final int CLIENTS_RECENCY = 604;
static final String DEFAULT_TABS_SORT_ORDER = Clients.LAST_MODIFIED + " DESC, " + Tabs.LAST_USED + " DESC";
static final String DEFAULT_CLIENTS_SORT_ORDER = Clients.LAST_MODIFIED + " DESC";
static final String DEFAULT_CLIENTS_RECENCY_SORT_ORDER = "COALESCE(MAX(" + Tabs.LAST_USED + "), " + Clients.LAST_MODIFIED + ") DESC";
static final String INDEX_TABS_GUID = "tabs_guid_index";
static final String INDEX_TABS_POSITION = "tabs_position_index";
@ -47,12 +49,14 @@ public class TabsProvider extends PerProfileDatabaseProvider<TabsProvider.TabsDa
static final Map<String, String> TABS_PROJECTION_MAP;
static final Map<String, String> CLIENTS_PROJECTION_MAP;
static final Map<String, String> CLIENTS_RECENCY_PROJECTION_MAP;
static {
URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "tabs", TABS);
URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "tabs/#", TABS_ID);
URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "clients", CLIENTS);
URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "clients/#", CLIENTS_ID);
URI_MATCHER.addURI(BrowserContract.TABS_AUTHORITY, "clients_recency", CLIENTS_RECENCY);
HashMap<String, String> map;
@ -76,10 +80,24 @@ public class TabsProvider extends PerProfileDatabaseProvider<TabsProvider.TabsDa
map.put(Clients.LAST_MODIFIED, Clients.LAST_MODIFIED);
map.put(Clients.DEVICE_TYPE, Clients.DEVICE_TYPE);
CLIENTS_PROJECTION_MAP = Collections.unmodifiableMap(map);
map = new HashMap<>();
map.put(Clients.GUID, projectColumn(TABLE_CLIENTS, Clients.GUID) + " AS guid");
map.put(Clients.NAME, projectColumn(TABLE_CLIENTS, Clients.NAME) + " AS name");
map.put(Clients.LAST_MODIFIED, projectColumn(TABLE_CLIENTS, Clients.LAST_MODIFIED) + " AS last_modified");
map.put(Clients.DEVICE_TYPE, projectColumn(TABLE_CLIENTS, Clients.DEVICE_TYPE) + " AS device_type");
// last_used is the max of the tab last_used times, or if there are no tabs,
// the client's last_modified time.
map.put(Tabs.LAST_USED, "COALESCE(MAX(" + projectColumn(TABLE_TABS, Tabs.LAST_USED) + "), " + projectColumn(TABLE_CLIENTS, Clients.LAST_MODIFIED) + ") AS last_used");
CLIENTS_RECENCY_PROJECTION_MAP = Collections.unmodifiableMap(map);
}
private static final String projectColumn(String table, String column) {
return table + "." + column;
}
private static final String selectColumn(String table, String column) {
return table + "." + column + " = ?";
return projectColumn(table, column) + " = ?";
}
final class TabsDatabaseHelper extends SQLiteOpenHelper {
@ -334,6 +352,7 @@ public class TabsProvider extends PerProfileDatabaseProvider<TabsProvider.TabsDa
SQLiteDatabase db = getReadableDatabase(uri);
final int match = URI_MATCHER.match(uri);
String groupBy = null;
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
String limit = uri.getQueryParameter(BrowserContract.PARAM_LIMIT);
@ -374,12 +393,27 @@ public class TabsProvider extends PerProfileDatabaseProvider<TabsProvider.TabsDa
qb.setTables(TABLE_CLIENTS);
break;
case CLIENTS_RECENCY:
trace("Query is on CLIENTS_RECENCY: " + uri);
if (TextUtils.isEmpty(sortOrder)) {
sortOrder = DEFAULT_CLIENTS_RECENCY_SORT_ORDER;
} else {
debug("Using sort order " + sortOrder + ".");
}
qb.setProjectionMap(CLIENTS_RECENCY_PROJECTION_MAP);
qb.setTables(TABLE_CLIENTS + " LEFT OUTER JOIN " + TABLE_TABS +
" ON (" + projectColumn(TABLE_CLIENTS, Clients.GUID) +
" = " + projectColumn(TABLE_TABS,Tabs.CLIENT_GUID) + ")");
groupBy = projectColumn(TABLE_CLIENTS, Clients.GUID);
break;
default:
throw new UnsupportedOperationException("Unknown query URI " + uri);
}
trace("Running built query.");
final Cursor cursor = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder, limit);
final Cursor cursor = qb.query(db, projection, selection, selectionArgs, groupBy, null, sortOrder, limit);
cursor.setNotificationUri(getContext().getContentResolver(), BrowserContract.TABS_AUTHORITY_URI);
return cursor;

View File

@ -95,6 +95,7 @@ public class FxAccountStatusFragment
private int debugClickCount = 0;
protected PreferenceCategory accountCategory;
protected Preference profilePreference;
protected Preference emailPreference;
protected Preference authServerPreference;
@ -160,7 +161,13 @@ public class FxAccountStatusFragment
addPreferencesFromResource(R.xml.fxaccount_status_prefscreen);
accountCategory = (PreferenceCategory) ensureFindPreference("signed_in_as_category");
profilePreference = ensureFindPreference("profile");
emailPreference = ensureFindPreference("email");
if (AppConstants.MOZ_ANDROID_FIREFOX_ACCOUNT_PROFILES) {
accountCategory.removePreference(emailPreference);
} else {
accountCategory.removePreference(profilePreference);
}
authServerPreference = ensureFindPreference("auth_server");
needsPasswordPreference = ensureFindPreference("needs_credentials");
@ -184,7 +191,11 @@ public class FxAccountStatusFragment
ALWAYS_SHOW_SYNC_SERVER = true;
}
emailPreference.setOnPreferenceClickListener(this);
if (AppConstants.MOZ_ANDROID_FIREFOX_ACCOUNT_PROFILES) {
profilePreference.setOnPreferenceClickListener(this);
} else {
emailPreference.setOnPreferenceClickListener(this);
}
needsPasswordPreference.setOnPreferenceClickListener(this);
needsVerificationPreference.setOnPreferenceClickListener(this);
@ -222,7 +233,8 @@ public class FxAccountStatusFragment
@Override
public boolean onPreferenceClick(Preference preference) {
if (preference == emailPreference) {
final Preference personalInformationPreference = AppConstants.MOZ_ANDROID_FIREFOX_ACCOUNT_PROFILES ? profilePreference : emailPreference;
if (preference == personalInformationPreference) {
debugClickCount += 1;
if (NUMBER_OF_CLICKS_TO_TOGGLE_DEBUG > 0 && debugClickCount >= NUMBER_OF_CLICKS_TO_TOGGLE_DEBUG) {
debugClickCount = 0;
@ -493,7 +505,15 @@ public class FxAccountStatusFragment
throw new IllegalArgumentException("fxAccount must not be null");
}
emailPreference.setTitle(fxAccount.getEmail());
if (AppConstants.MOZ_ANDROID_FIREFOX_ACCOUNT_PROFILES) {
if (AppConstants.Versions.feature11Plus) {
profilePreference.setIcon(getResources().getDrawable(R.drawable.sync_avatar_default));
}
profilePreference.setTitle(fxAccount.getAndroidAccount().name);
} else {
emailPreference.setTitle(fxAccount.getEmail());
}
updateAuthServerPreference();
updateSyncServerPreference();

View File

@ -421,7 +421,9 @@ size. -->
<!ENTITY site_settings_clear "Clear">
<!ENTITY site_settings_no_settings "There are no settings to clear.">
<!ENTITY reading_list_added "Page added to your Reading List">
<!-- Localization note (reading_list_added2) : Used in a toast, please keep as short
as possible. -->
<!ENTITY reading_list_added2 "Added to list">
<!ENTITY reading_list_removed "Page removed from your Reading List">
<!-- Localization note (reading_list_remove) : Used to remove the currently open page from
the user's reading list. The opposite of overlay_share_reading_list_btn_label. -->

View File

@ -744,7 +744,7 @@ ANDROID_GENERATED_RESFILES += [
for var in ('MOZ_ANDROID_ANR_REPORTER', 'MOZ_LINKER_EXTRACT', 'MOZILLA_OFFICIAL', 'MOZ_DEBUG',
'MOZ_ANDROID_SEARCH_ACTIVITY', 'MOZ_NATIVE_DEVICES', 'MOZ_ANDROID_MLS_STUMBLER',
'MOZ_ANDROID_SHARE_OVERLAY', 'MOZ_ANDROID_DOWNLOADS_INTEGRATION', 'MOZ_INSTALL_TRACKING',
'MOZ_ANDROID_TAB_QUEUE'):
'MOZ_ANDROID_TAB_QUEUE', 'MOZ_ANDROID_FIREFOX_ACCOUNT_PROFILES'):
if CONFIG[var]:
DEFINES[var] = 1

View File

@ -10,11 +10,17 @@ import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.os.Bundle;
import android.os.Parcelable;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.R;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.RemoteClient;
import org.mozilla.gecko.db.TabsAccessor;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.activities.FxAccountGetStartedActivity;
@ -28,19 +34,14 @@ import org.mozilla.gecko.sync.CommandRunner;
import org.mozilla.gecko.sync.GlobalSession;
import org.mozilla.gecko.sync.SyncConfiguration;
import org.mozilla.gecko.sync.SyncConstants;
import org.mozilla.gecko.sync.repositories.NullCursorException;
import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
import org.mozilla.gecko.sync.setup.SyncAccounts;
import org.mozilla.gecko.sync.syncadapter.SyncAdapter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
@ -54,8 +55,8 @@ public class SendTab extends ShareMethod {
// Key used in the extras Bundle in the share intent used for a send tab ShareMethod.
public static final String SEND_TAB_TARGET_DEVICES = "SEND_TAB_TARGET_DEVICES";
// Key used in broadcast intent from SendTab ShareMethod specifying available ClientRecords.
public static final String EXTRA_CLIENT_RECORDS = "RECORDS";
// Key used in broadcast intent from SendTab ShareMethod specifying available RemoteClients.
public static final String EXTRA_REMOTE_CLIENT_RECORDS = "RECORDS";
// The intent we should dispatch when the button for this ShareMethod is tapped, instead of
// taking the normal action (e.g., "Set up Sync!")
@ -190,20 +191,16 @@ public class SendTab extends ShareMethod {
* Load the list of Sync clients that are not this device using the given TabSender.
*/
private void updateClientList(TabSender tabSender) {
Collection<ClientRecord> otherClients = getOtherClients(tabSender);
Collection<RemoteClient> otherClients = getOtherClients(tabSender);
// Put the list of RemoteClients into the uiStateIntent and broadcast it.
RemoteClient[] records = new RemoteClient[otherClients.size()];
records = otherClients.toArray(records);
ParcelableClientRecord[] records = new ParcelableClientRecord[otherClients.size()];
validGUIDs = new HashSet<>();
int i = 0;
// Put the list of ClientRecords into the uiStateIntent and broadcast it.
for (ClientRecord client : otherClients) {
ParcelableClientRecord record = ParcelableClientRecord.fromClientRecord(client);
records[i] = record;
validGUIDs.add(record.guid);
i++;
for (RemoteClient client : otherClients) {
validGUIDs.add(client.guid);
}
if (validGUIDs.isEmpty()) {
@ -214,7 +211,7 @@ public class SendTab extends ShareMethod {
}
Intent uiStateIntent = getUIStateIntent();
uiStateIntent.putExtra(EXTRA_CLIENT_RECORDS, records);
uiStateIntent.putExtra(EXTRA_REMOTE_CLIENT_RECORDS, records);
broadcastUIState(uiStateIntent);
}
@ -252,48 +249,25 @@ public class SendTab extends ShareMethod {
}
/**
* @return A map from GUID to client record for all sync clients, including our own; or null iff
* ClientsDatabaseAccessor.fetchAllClients throws NullCursorException.
* @return A collection of unique remote clients sorted by most recently used.
*/
protected Map<String, ClientRecord> getAllClients() {
ClientsDatabaseAccessor db = new ClientsDatabaseAccessor(context);
try {
return db.fetchAllClients();
} catch (NullCursorException e) {
Log.w(LOGTAG, "NullCursorException while populating device list.", e);
return null;
} finally {
db.close();
}
}
/**
* @return a collection of client records, excluding our own.
*/
protected Collection<ClientRecord> getOtherClients(final TabSender sender) {
protected Collection<RemoteClient> getOtherClients(final TabSender sender) {
if (sender == null) {
Log.w(LOGTAG, "No tab sender when fetching other client IDs.");
return Collections.emptyList();
}
final Map<String, ClientRecord> all = getAllClients();
if (all == null) {
return Collections.emptyList();
}
final String ourGUID = sender.getAccountGUID();
if (ourGUID == null) {
return all.values();
}
final ArrayList<ClientRecord> out = new ArrayList<>(all.size());
for (Map.Entry<String, ClientRecord> entry : all.entrySet()) {
if (!ourGUID.equals(entry.getKey())) {
out.add(entry.getValue());
final BrowserDB browserDB = GeckoProfile.get(context).getDB();
final TabsAccessor tabsAccessor = browserDB.getTabsAccessor();
final Cursor remoteTabsCursor = tabsAccessor.getRemoteClientsByRecencyCursor(context);
try {
if (remoteTabsCursor.getCount() == 0) {
return Collections.emptyList();
}
return tabsAccessor.getClientsWithoutTabsByRecencyFromCursor(remoteTabsCursor);
} finally {
remoteTabsCursor.close();
}
return out;
}
@Override

View File

@ -9,6 +9,7 @@ import java.util.Collection;
import org.mozilla.gecko.AppConstants;
import org.mozilla.gecko.Assert;
import org.mozilla.gecko.R;
import org.mozilla.gecko.db.RemoteClient;
import org.mozilla.gecko.overlays.service.sharemethods.ParcelableClientRecord;
import org.mozilla.gecko.overlays.ui.SendTabList.State;
@ -22,19 +23,19 @@ import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.TextView;
public class SendTabDeviceListArrayAdapter extends ArrayAdapter<ParcelableClientRecord> {
public class SendTabDeviceListArrayAdapter extends ArrayAdapter<RemoteClient> {
@SuppressWarnings("unused")
private static final String LOGTAG = "GeckoSendTabAdapter";
private State currentState;
// String to display when in a "button-like" special state. Instead of using a
// ParcelableClientRecord we override the rendering using this string.
// RemoteClient we override the rendering using this string.
private String dummyRecordName;
private final SendTabTargetSelectedListener listener;
private Collection<ParcelableClientRecord> records;
private Collection<RemoteClient> records;
// The AlertDialog to show in the event the record is pressed while in the SHOW_DEVICES state.
// This will show the user a prompt to select a device from a longer list of devices.
@ -53,12 +54,12 @@ public class SendTabDeviceListArrayAdapter extends ArrayAdapter<ParcelableClient
* Get an array of the contents of this adapter were it in the LIST state.
* Useful for determining the "real" contents of the adapter.
*/
public ParcelableClientRecord[] toArray() {
return records.toArray(new ParcelableClientRecord[records.size()]);
public RemoteClient[] toArray() {
return records.toArray(new RemoteClient[records.size()]);
}
public void setClientRecordList(Collection<ParcelableClientRecord> clientRecordList) {
records = clientRecordList;
public void setRemoteClientsList(Collection<RemoteClient> remoteClientsList) {
records = remoteClientsList;
updateRecordList();
}
@ -77,7 +78,7 @@ public class SendTabDeviceListArrayAdapter extends ArrayAdapter<ParcelableClient
if (AppConstants.Versions.feature11Plus) {
addAll(records);
} else {
for (ParcelableClientRecord record : records) {
for (RemoteClient record : records) {
add(record);
}
}
@ -122,13 +123,13 @@ public class SendTabDeviceListArrayAdapter extends ArrayAdapter<ParcelableClient
}
// The remaining states delegate to the SentTabTargetSelectedListener.
final ParcelableClientRecord clientRecord = getItem(position);
final RemoteClient remoteClient = getItem(position);
if (currentState == State.LIST) {
final Drawable clientIcon = context.getResources().getDrawable(getImage(clientRecord));
row.setText(clientRecord.name);
final Drawable clientIcon = context.getResources().getDrawable(getImage(remoteClient));
row.setText(remoteClient.name);
row.setDrawable(clientIcon);
final String listenerGUID = clientRecord.guid;
final String listenerGUID = remoteClient.guid;
row.setOnClickListener(new OnClickListener() {
@Override
@ -148,8 +149,8 @@ public class SendTabDeviceListArrayAdapter extends ArrayAdapter<ParcelableClient
return row;
}
private static int getImage(ParcelableClientRecord record) {
if ("mobile".equals(record.type)) {
private static int getImage(RemoteClient record) {
if ("mobile".equals(record.deviceType)) {
return R.drawable.device_mobile;
}

View File

@ -4,7 +4,6 @@
package org.mozilla.gecko.overlays.ui;
import static org.mozilla.gecko.overlays.ui.SendTabList.State.LIST;
import static org.mozilla.gecko.overlays.ui.SendTabList.State.LOADING;
import static org.mozilla.gecko.overlays.ui.SendTabList.State.SHOW_DEVICES;
@ -15,13 +14,12 @@ import org.mozilla.gecko.Assert;
import org.mozilla.gecko.R;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.overlays.service.sharemethods.ParcelableClientRecord;
import org.mozilla.gecko.db.RemoteClient;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.ListAdapter;
import android.widget.ListView;
@ -110,10 +108,10 @@ public class SendTabList extends ListView {
}
}
public void setSyncClients(final ParcelableClientRecord[] c) {
final ParcelableClientRecord[] clients = c == null ? new ParcelableClientRecord[0] : c;
public void setSyncClients(final RemoteClient[] c) {
final RemoteClient[] clients = c == null ? new RemoteClient[0] : c;
clientListAdapter.setClientRecordList(Arrays.asList(clients));
clientListAdapter.setRemoteClientsList(Arrays.asList(clients));
}
/**
@ -131,7 +129,7 @@ public class SendTabList extends ListView {
builder = new AlertDialog.Builder(context);
}
final ParcelableClientRecord[] records = clientListAdapter.toArray();
final RemoteClient[] records = clientListAdapter.toArray();
final String[] dialogElements = new String[records.length];
for (int i = 0; i < records.length; i++) {

View File

@ -15,6 +15,7 @@ import org.mozilla.gecko.R;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.db.LocalBrowserDB;
import org.mozilla.gecko.db.RemoteClient;
import org.mozilla.gecko.overlays.OverlayConstants;
import org.mozilla.gecko.overlays.service.OverlayActionService;
import org.mozilla.gecko.overlays.service.sharemethods.ParcelableClientRecord;
@ -101,13 +102,13 @@ public class ShareDialog extends Locales.LocaleAwareActivity implements SendTabT
protected void handleSendTabUIEvent(Intent intent) {
sendTabOverrideIntent = intent.getParcelableExtra(SendTab.OVERRIDE_INTENT);
ParcelableClientRecord[] clientrecords = (ParcelableClientRecord[]) intent.getParcelableArrayExtra(SendTab.EXTRA_CLIENT_RECORDS);
RemoteClient[] remoteClientRecords = (RemoteClient[]) intent.getParcelableArrayExtra(SendTab.EXTRA_REMOTE_CLIENT_RECORDS);
// Escape hatch: we don't show the option to open this dialog in this state so this should
// never be run. However, due to potential inconsistencies in synced client state
// (e.g. bug 1122302 comment 47), we might fail.
if (state == State.DEVICES_ONLY &&
(clientrecords == null || clientrecords.length == 0)) {
(remoteClientRecords == null || remoteClientRecords.length == 0)) {
Log.e(LOGTAG, "In state: " + State.DEVICES_ONLY + " and received 0 synced clients. Finishing...");
Toast.makeText(this, getResources().getText(R.string.overlay_no_synced_devices), Toast.LENGTH_SHORT)
.show();
@ -115,11 +116,11 @@ public class ShareDialog extends Locales.LocaleAwareActivity implements SendTabT
return;
}
sendTabList.setSyncClients(clientrecords);
sendTabList.setSyncClients(remoteClientRecords);
if (state == State.DEVICES_ONLY ||
clientrecords == null ||
clientrecords.length <= MAXIMUM_INLINE_DEVICES) {
remoteClientRecords == null ||
remoteClientRecords.length <= MAXIMUM_INLINE_DEVICES) {
// Show the list of devices in-line.
sendTabList.switchState(SendTabList.State.LIST);
@ -128,7 +129,7 @@ public class ShareDialog extends Locales.LocaleAwareActivity implements SendTabT
//
// Note: a more thorough implementation would add this
// (and other non-ListView buttons) into a custom ListView.
if (clientrecords == null || clientrecords.length == 0) {
if (remoteClientRecords == null || remoteClientRecords.length == 0) {
readingListButton.setBackgroundResource(
R.drawable.overlay_share_button_background_first);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -695,8 +695,8 @@
<item name="android:layout_weight">1</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:layout_gravity">center_vertical</item>
<item name="android:ellipsize">end</item>
<item name="android:maxLines">1</item>
<item name="android:ellipsize">none</item>
<item name="android:maxLines">3</item>
<item name="android:clickable">false</item>
<item name="android:focusable">false</item>
</style>

View File

@ -5,6 +5,12 @@
<PreferenceCategory
android:key="signed_in_as_category"
android:title="@string/fxaccount_status_signed_in_as" >
<Preference
android:editable="false"
android:key="profile"
android:icon="@drawable/sync_avatar_default"
android:persistent="false"
android:title="" />
<Preference
android:editable="false"
android:key="email"

View File

@ -40,7 +40,7 @@
<org.mozilla.gecko.preferences.LinkPreference android:key="android.not_a_preference.signon.manage"
android:title="@string/pref_manage_logins"
url="about:passwords"/>
url="about:logins"/>
<CheckBoxPreference android:key="signon.rememberSignons"
android:title="@string/pref_remember_signons"

View File

@ -321,7 +321,7 @@
<string name="site_settings_clear">&site_settings_clear;</string>
<string name="site_settings_no_settings">&site_settings_no_settings;</string>
<string name="reading_list_added">&reading_list_added;</string>
<string name="reading_list_added">&reading_list_added2;</string>
<string name="reading_list_removed">&reading_list_removed;</string>
<string name="reading_list_remove">&reading_list_remove;</string>
<string name="reading_list_duplicate">&reading_list_duplicate;</string>

View File

@ -23,11 +23,13 @@ package org.mozilla.gecko.widget;
// Mozilla: New import
import android.accounts.Account;
import android.content.pm.PackageManager;
import org.mozilla.gecko.db.BrowserDB;
import org.mozilla.gecko.db.TabsAccessor;
import org.mozilla.gecko.distribution.Distribution;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.overlays.ui.ShareDialog;
import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
import org.mozilla.gecko.sync.setup.SyncAccounts;
import org.mozilla.gecko.R;
import java.io.File;
@ -38,6 +40,7 @@ import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ResolveInfo;
import android.database.Cursor;
import android.database.DataSetObservable;
import android.os.AsyncTask;
import android.text.TextUtils;
@ -1311,8 +1314,19 @@ public class ActivityChooserModel extends DataSetObservable {
return false;
}
final ClientsDatabaseAccessor db = new ClientsDatabaseAccessor(mContext);
return db.clientsCount() > 0;
final BrowserDB browserDB = GeckoProfile.get(mContext).getDB();
final TabsAccessor tabsAccessor = browserDB.getTabsAccessor();
final Cursor remoteClientsCursor = tabsAccessor
.getRemoteClientsByRecencyCursor(mContext);
if (remoteClientsCursor == null) {
return false;
}
try {
return remoteClientsCursor.getCount() > 0;
} finally {
remoteClientsCursor.close();
}
}
/**

View File

@ -20,9 +20,9 @@ XPCOMUtils.defineLazyGetter(window, "gChromeWin", function()
XPCOMUtils.defineLazyModuleGetter(this, "Prompt",
"resource://gre/modules/Prompt.jsm");
let debug = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.d.bind(null, "AboutPasswords");
let debug = Cu.import("resource://gre/modules/AndroidLog.jsm", {}).AndroidLog.d.bind(null, "AboutLogins");
let gStringBundle = Services.strings.createBundle("chrome://browser/locale/aboutPasswords.properties");
let gStringBundle = Services.strings.createBundle("chrome://browser/locale/aboutLogins.properties");
function copyStringAndToast(string, notifyString) {
try {
@ -30,15 +30,15 @@ function copyStringAndToast(string, notifyString) {
clipboard.copyString(string);
gChromeWin.NativeWindow.toast.show(notifyString, "short");
} catch (e) {
debug("Error copying from about:passwords");
gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("passwordsDetails.copyFailed"), "short");
debug("Error copying from about:logins");
gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("loginsDetails.copyFailed"), "short");
}
}
// Delay filtering while typing in MS
const FILTER_DELAY = 500;
let Passwords = {
let Logins = {
_logins: [],
_filterTimer: null,
@ -157,11 +157,11 @@ let Passwords = {
window: window,
});
let menuItems = [
{ label: gStringBundle.GetStringFromName("passwordsMenu.showPassword") },
{ label: gStringBundle.GetStringFromName("passwordsMenu.copyPassword") },
{ label: gStringBundle.GetStringFromName("passwordsMenu.copyUsername") },
{ label: gStringBundle.GetStringFromName("passwordsMenu.details") },
{ label: gStringBundle.GetStringFromName("passwordsMenu.delete") }
{ label: gStringBundle.GetStringFromName("loginsMenu.showPassword") },
{ label: gStringBundle.GetStringFromName("loginsMenu.copyPassword") },
{ label: gStringBundle.GetStringFromName("loginsMenu.copyUsername") },
{ label: gStringBundle.GetStringFromName("loginsMenu.details") },
{ label: gStringBundle.GetStringFromName("loginsMenu.delete") }
];
prompt.setSingleChoiceItems(menuItems);
@ -173,21 +173,21 @@ let Passwords = {
window: window,
message: login.password,
buttons: [
gStringBundle.GetStringFromName("passwordsDialog.copy"),
gStringBundle.GetStringFromName("passwordsDialog.cancel") ]
gStringBundle.GetStringFromName("loginsDialog.copy"),
gStringBundle.GetStringFromName("loginsDialog.cancel") ]
}).show((data) => {
switch (data.button) {
case 0:
// Corresponds to "Copy password" button.
copyStringAndToast(login.password, gStringBundle.GetStringFromName("passwordsDetails.passwordCopied"));
copyStringAndToast(login.password, gStringBundle.GetStringFromName("loginsDetails.passwordCopied"));
}
});
break;
case 1:
copyStringAndToast(login.password, gStringBundle.GetStringFromName("passwordsDetails.passwordCopied"));
copyStringAndToast(login.password, gStringBundle.GetStringFromName("loginsDetails.passwordCopied"));
break;
case 2:
copyStringAndToast(login.username, gStringBundle.GetStringFromName("passwordsDetails.usernameCopied"));
copyStringAndToast(login.username, gStringBundle.GetStringFromName("loginsDetails.usernameCopied"));
break;
case 3:
this._showDetails(loginItem);
@ -196,10 +196,10 @@ let Passwords = {
case 4:
let confirmPrompt = new Prompt({
window: window,
message: gStringBundle.GetStringFromName("passwordsDialog.confirmDelete"),
message: gStringBundle.GetStringFromName("loginsDialog.confirmDelete"),
buttons: [
gStringBundle.GetStringFromName("passwordsDialog.confirm"),
gStringBundle.GetStringFromName("passwordsDialog.cancel") ]
gStringBundle.GetStringFromName("loginsDialog.confirm"),
gStringBundle.GetStringFromName("loginsDialog.cancel") ]
});
confirmPrompt.show((data) => {
switch (data.button) {
@ -287,7 +287,7 @@ let Passwords = {
observe: function (subject, topic, data) {
switch(topic) {
case "passwordmgr-storage-changed": {
// Reload passwords content.
// Reload logins content.
this._loadList(this._getLogins());
break;
}
@ -317,7 +317,7 @@ let Passwords = {
let lastChanged = new Date(login.QueryInterface(Ci.nsILoginMetaInfo).timePasswordChanged);
let days = Math.round((Date.now() - lastChanged) / 1000 / 60 / 60/ 24);
document.getElementById("detail-age").textContent = gStringBundle.formatStringFromName("passwordsDetails.age", [days], 1);
document.getElementById("detail-age").textContent = gStringBundle.formatStringFromName("loginsDetails.age", [days], 1);
let list = document.getElementById("logins-list");
list.setAttribute("hidden", "true");
@ -334,13 +334,13 @@ let Passwords = {
_copyUsername: function() {
let detailItem = document.querySelector("#login-details > .login-item");
let login = detailItem.login;
copyStringAndToast(login.username, gStringBundle.GetStringFromName("passwordsDetails.usernameCopied"));
copyStringAndToast(login.username, gStringBundle.GetStringFromName("loginsDetails.usernameCopied"));
},
_copyPassword: function() {
let detailItem = document.querySelector("#login-details > .login-item");
let login = detailItem.login;
copyStringAndToast(login.password, gStringBundle.GetStringFromName("passwordsDetails.passwordCopied"));
copyStringAndToast(login.password, gStringBundle.GetStringFromName("loginsDetails.passwordCopied"));
},
_openLink: function (clickEvent) {
@ -370,5 +370,5 @@ let Passwords = {
}
};
window.addEventListener("load", Passwords.init.bind(Passwords), false);
window.addEventListener("unload", Passwords.uninit.bind(Passwords), false);
window.addEventListener("load", Logins.init.bind(Logins), false);
window.addEventListener("unload", Logins.uninit.bind(Logins), false);

View File

@ -5,7 +5,7 @@
%globalDTD;
<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
%brandDTD;
<!ENTITY % aboutDTD SYSTEM "chrome://browser/locale/aboutPasswords.dtd" >
<!ENTITY % aboutDTD SYSTEM "chrome://browser/locale/aboutLogins.dtd" >
%aboutDTD;
]>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
@ -13,16 +13,16 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>&aboutPasswords.title;</title>
<title>&aboutLogins.title;</title>
<meta name="viewport" content="width=device-width; user-scalable=0" />
<link rel="icon" type="image/png" sizes="64x64" href="chrome://branding/content/favicon64.png" />
<link rel="stylesheet" href="chrome://browser/skin/aboutBase.css" type="text/css"/>
<link rel="stylesheet" href="chrome://browser/skin/aboutPasswords.css" type="text/css"/>
<script type="application/javascript;version=1.8" src="chrome://browser/content/aboutPasswords.js"></script>
<link rel="stylesheet" href="chrome://browser/skin/aboutLogins.css" type="text/css"/>
<script type="application/javascript;version=1.8" src="chrome://browser/content/aboutLogins.js"></script>
</head>
<body dir="&locale.dir;">
<div id="passwords-header" class="header">
<div>&aboutPasswords.title;</div>
<div id="logins-header" class="header">
<div>&aboutLogins.title;</div>
<ul class="toolbar-buttons">
<li id="filter-button"></li>
</ul>
@ -41,8 +41,8 @@
<div id="detail-age"></div>
</div>
<div class="buttons">
<button id="copyusername-btn">&aboutPasswords.copyUsername;</button>
<button id="copypassword-btn">&aboutPasswords.copyPassword;</button>
<button id="copyusername-btn">&aboutLogins.copyUsername;</button>
<button id="copypassword-btn">&aboutLogins.copyPassword;</button>
</div>
</div>
</div>

View File

@ -6300,6 +6300,20 @@ var XPInstallObserver = {
},
onInstallFailed: function(aInstall) {
this._showErrorMessage(aInstall);
},
onDownloadProgress: function(aInstall) {},
onDownloadFailed: function(aInstall) {
this._showErrorMessage(aInstall);
},
onDownloadCancelled: function(aInstall) {
this._showErrorMessage(aInstall);
},
_showErrorMessage: function(aInstall) {
// Don't create a notification for distribution add-ons.
if (Distribution.pendingAddonInstalls.has(aInstall)) {
Cu.reportError("Error installing distribution add-on: " + aInstall.addon.id);
@ -6307,39 +6321,44 @@ var XPInstallObserver = {
return;
}
NativeWindow.toast.show(Strings.browser.GetStringFromName("alertAddonsFail"), "short");
},
onDownloadProgress: function xpidm_onDownloadProgress(aInstall) {},
onDownloadFailed: function(aInstall) {
this.onInstallFailed(aInstall);
},
onDownloadCancelled: function(aInstall) {
let host = (aInstall.originatingURI instanceof Ci.nsIStandardURL) && aInstall.originatingURI.host;
if (!host)
if (!host) {
host = (aInstall.sourceURI instanceof Ci.nsIStandardURL) && aInstall.sourceURI.host;
}
let error = (host || aInstall.error == 0) ? "addonError" : "addonLocalError";
if (aInstall.error != 0)
if (aInstall.error < 0) {
error += aInstall.error;
else if (aInstall.addon && aInstall.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED)
} else if (aInstall.addon && aInstall.addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED) {
error += "Blocklisted";
else if (aInstall.addon && (!aInstall.addon.isCompatible || !aInstall.addon.isPlatformCompatible))
} else {
error += "Incompatible";
else
return; // No need to show anything in this case.
}
let msg = Strings.browser.GetStringFromName(error);
// TODO: formatStringFromName
msg = msg.replace("#1", aInstall.name);
if (host)
if (host) {
msg = msg.replace("#2", host);
}
msg = msg.replace("#3", Strings.brand.GetStringFromName("brandShortName"));
msg = msg.replace("#4", Services.appinfo.version);
NativeWindow.toast.show(msg, "short");
if (aInstall.error == AddonManager.ERROR_SIGNEDSTATE_REQUIRED) {
new Prompt({
window: window,
title: Strings.browser.GetStringFromName("addonError.titleBlocked"),
message: msg,
buttons: [Strings.browser.GetStringFromName("addonError.learnMore")]
}).show((data) => {
if (data.button === 0) {
let url = Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons";
BrowserApp.addTab(url, { parentId: BrowserApp.selectedTab.id });
}
});
} else {
Services.prompt.alert(null, Strings.browser.GetStringFromName("addonError.titleError"), msg);
}
},
showRestartPrompt: function() {

View File

@ -61,8 +61,8 @@ chrome.jar:
content/aboutDevices.js (content/aboutDevices.js)
#endif
#ifdef NIGHTLY_BUILD
content/aboutPasswords.xhtml (content/aboutPasswords.xhtml)
content/aboutPasswords.js (content/aboutPasswords.js)
content/aboutLogins.xhtml (content/aboutLogins.xhtml)
content/aboutLogins.js (content/aboutLogins.js)
content/WebcompatReporter.js (content/WebcompatReporter.js)
#endif
#ifdef MOZ_PAY

View File

@ -88,8 +88,8 @@ if (AppConstants.MOZ_DEVICES) {
};
}
if (AppConstants.NIGHTLY_BUILD) {
modules['passwords'] = {
uri: "chrome://browser/content/aboutPasswords.xhtml",
modules['logins'] = {
uri: "chrome://browser/content/aboutLogins.xhtml",
privileged: true
};
}

View File

@ -22,7 +22,7 @@ contract @mozilla.org/network/protocol/about;1?what=blocked {322ba47e-7047-4f71-
contract @mozilla.org/network/protocol/about;1?what=devices {322ba47e-7047-4f71-aebf-cb7d69325cd9}
#endif
#ifdef NIGHTLY_BUILD
contract @mozilla.org/network/protocol/about;1?what=passwords {322ba47e-7047-4f71-aebf-cb7d69325cd9}
contract @mozilla.org/network/protocol/about;1?what=logins {322ba47e-7047-4f71-aebf-cb7d69325cd9}
#endif
# DirectoryProvider.js

View File

@ -107,6 +107,11 @@ fi
# Use the low-memory GC tuning.
export JS_GC_SMALL_CHUNK_SIZE=1
# Enable FxAccount Avatar
if test "$NIGHTLY_BUILD"; then
MOZ_ANDROID_FIREFOX_ACCOUNT_PROFILES=1
fi
# Enable checking that add-ons are signed by the trusted root
MOZ_ADDON_SIGNING=1
if test "$MOZ_OFFICIAL_BRANDING"; then

View File

@ -2,7 +2,7 @@
- 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/. -->
<!ENTITY aboutPasswords.title "Passwords">
<!ENTITY aboutLogins.title "Logins">
<!ENTITY aboutPasswords.copyUsername "Copy Username">
<!ENTITY aboutPasswords.copyPassword "Copy Password">
<!ENTITY aboutLogins.copyUsername "Copy Username">
<!ENTITY aboutLogins.copyPassword "Copy Password">

View File

@ -0,0 +1,20 @@
# 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/.
loginsMenu.showPassword=Show password
loginsMenu.copyPassword=Copy password
loginsMenu.copyUsername=Copy username
loginsMenu.details=Details
loginsMenu.delete=Delete
loginsDialog.confirmDelete=Delete this login?
loginsDialog.copy=Copy
loginsDialog.confirm=OK
loginsDialog.cancel=Cancel
loginsDetails.age=Age: %S days
loginsDetails.copyFailed=Copy failed
loginsDetails.passwordCopied=Password copied
loginsDetails.usernameCopied=Username copied

View File

@ -1,20 +0,0 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
passwordsMenu.showPassword=Show password
passwordsMenu.copyPassword=Copy password
passwordsMenu.copyUsername=Copy username
passwordsMenu.details=Details
passwordsMenu.delete=Delete
passwordsDialog.confirmDelete=Delete this login?
passwordsDialog.copy=Copy
passwordsDialog.confirm=OK
passwordsDialog.cancel=Cancel
passwordsDetails.age=Age: %S days
passwordsDetails.copyFailed=Copy failed
passwordsDetails.passwordCopied=Password copied
passwordsDetails.usernameCopied=Username copied

View File

@ -7,7 +7,6 @@ addonsConfirmInstall.install=Install
# Alerts
alertAddonsDownloading=Downloading add-on
alertAddonsFail=Installation failed
alertAddonsInstalledNoRestart.message=Installation complete
# LOCALIZATION NOTE (alertAddonsInstalledNoRestart.action2): Ideally, this string is short (it's a
@ -39,19 +38,25 @@ alertSearchEngineDuplicateToast='%S' is already one of your search engines
downloadCancelPromptTitle=Cancel Download
downloadCancelPromptMessage=Do you want to cancel this download?
# LOCALIZATION NOTE (addonError-1, addonError-2, addonError-3, addonError-4):
addonError.titleError=Error
addonError.titleBlocked=Blocked add-on
addonError.learnMore=Learn more
# LOCALIZATION NOTE (addonError-1, addonError-2, addonError-3, addonError-4, addonError-5):
# #1 is the add-on name, #2 is the add-on host, #3 is the application name
addonError-1=The add-on could not be downloaded because of a connection failure on #2.
addonError-2=The add-on from #2 could not be installed because it does not match the add-on #3 expected.
addonError-3=The add-on downloaded from #2 could not be installed because it appears to be corrupt.
addonError-4=#1 could not be installed because #3 cannot modify the needed file.
addonError-5=#3 has prevented #2 from installing an unverified add-on.
# LOCALIZATION NOTE (addonLocalError-1, addonLocalError-2, addonLocalError-3, addonLocalError-4, addonErrorIncompatible, addonErrorBlocklisted):
# LOCALIZATION NOTE (addonLocalError-1, addonLocalError-2, addonLocalError-3, addonLocalError-4, addonLocalError-5, addonErrorIncompatible, addonErrorBlocklisted):
# #1 is the add-on name, #3 is the application name, #4 is the application version
addonLocalError-1=This add-on could not be installed because of a filesystem error.
addonLocalError-2=This add-on could not be installed because it does not match the add-on #3 expected.
addonLocalError-3=This add-on could not be installed because it appears to be corrupt.
addonLocalError-4=#1 could not be installed because #3 cannot modify the needed file.
addonLocalError-5=This add-on could not be installed because it has not been verified.
addonErrorIncompatible=#1 could not be installed because it is not compatible with #3 #4.
addonErrorBlocklisted=#1 could not be installed because it has a high risk of causing stability or security problems.

View File

@ -39,8 +39,8 @@
locale/@AB_CD@/browser/handling.properties (%chrome/handling.properties)
locale/@AB_CD@/browser/webapp.properties (%chrome/webapp.properties)
#ifdef NIGHTLY_BUILD
locale/@AB_CD@/browser/aboutPasswords.dtd (%chrome/aboutPasswords.dtd)
locale/@AB_CD@/browser/aboutPasswords.properties (%chrome/aboutPasswords.properties)
locale/@AB_CD@/browser/aboutLogins.dtd (%chrome/aboutLogins.dtd)
locale/@AB_CD@/browser/aboutLogins.properties (%chrome/aboutLogins.properties)
locale/@AB_CD@/browser/webcompatReporter.properties (%chrome/webcompatReporter.properties)
#endif

View File

@ -18,6 +18,7 @@ jar.sources += [
'src/org/mozilla/tests/browser/junit3/TestImageDownloader.java',
'src/org/mozilla/tests/browser/junit3/TestJarReader.java',
'src/org/mozilla/tests/browser/junit3/TestRawResource.java',
'src/org/mozilla/tests/browser/junit3/TestRemoteTabs.java',
'src/org/mozilla/tests/browser/junit3/TestSuggestedSites.java',
'src/org/mozilla/tests/browser/junit3/TestTopSitesCursorWrapper.java',
]

View File

@ -0,0 +1,127 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.tests.browser.junit3;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.RemoteException;
import android.test.InstrumentationTestCase;
import org.mozilla.gecko.background.db.CursorDumper;
import org.mozilla.gecko.db.BrowserContract;
import org.mozilla.gecko.db.LocalTabsAccessor;
import org.mozilla.gecko.db.RemoteClient;
import org.mozilla.gecko.db.TabsAccessor;
import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
import java.util.List;
public class TestRemoteTabs extends InstrumentationTestCase {
public void testGetClientsWithoutTabsByRecencyFromCursor() throws Exception {
final Uri uri = BrowserContractHelpers.CLIENTS_CONTENT_URI;
final ContentResolver cr = getInstrumentation().getTargetContext().getContentResolver();
final ContentProviderClient cpc = cr.acquireContentProviderClient(uri);
final LocalTabsAccessor accessor = new LocalTabsAccessor("test"); // The profile name given doesn't matter.
try {
// Delete all tabs to begin with.
cpc.delete(uri, null, null);
Cursor allClients = cpc.query(uri, null, null, null, null);
try {
assertEquals(0, allClients.getCount());
} finally {
allClients.close();
}
// Insert a local and remote1 client record, neither with tabs.
final long now = System.currentTimeMillis();
// Local client has GUID = null.
final ContentValues local = new ContentValues();
local.put(BrowserContract.Clients.NAME, "local");
local.put(BrowserContract.Clients.LAST_MODIFIED, now + 1);
// Remote clients have GUID != null.
final ContentValues remote1 = new ContentValues();
remote1.put(BrowserContract.Clients.GUID, "guid1");
remote1.put(BrowserContract.Clients.NAME, "remote1");
remote1.put(BrowserContract.Clients.LAST_MODIFIED, now + 2);
final ContentValues remote2 = new ContentValues();
remote2.put(BrowserContract.Clients.GUID, "guid2");
remote2.put(BrowserContract.Clients.NAME, "remote2");
remote2.put(BrowserContract.Clients.LAST_MODIFIED, now + 3);
ContentValues[] values = new ContentValues[]{local, remote1, remote2};
int inserted = cpc.bulkInsert(uri, values);
assertEquals(3, inserted);
allClients = cpc.query(BrowserContract.Clients.CONTENT_RECENCY_URI, null, null, null, null);
try {
CursorDumper.dumpCursor(allClients);
// The local client is not ignored.
assertEquals(3, allClients.getCount());
final List<RemoteClient> clients = accessor.getClientsWithoutTabsByRecencyFromCursor(allClients);
assertEquals(3, clients.size());
for (RemoteClient client : clients) {
// Each client should not have any tabs.
assertNotNull(client.tabs);
assertEquals(0, client.tabs.size());
}
// Since there are no tabs, the order should be based on last_modified.
assertEquals("guid2", clients.get(0).guid);
assertEquals("guid1", clients.get(1).guid);
assertEquals(null, clients.get(2).guid);
} finally {
allClients.close();
}
// Now let's add a few tabs to one client. The times are chosen so that one tab's
// last used is not relevant, and the other tab is the most recent used.
final ContentValues remoteTab1 = new ContentValues();
remoteTab1.put(BrowserContract.Tabs.CLIENT_GUID, "guid1");
remoteTab1.put(BrowserContract.Tabs.TITLE, "title1");
remoteTab1.put(BrowserContract.Tabs.URL, "http://test.com/test1");
remoteTab1.put(BrowserContract.Tabs.HISTORY, "[\"http://test.com/test1\"]");
remoteTab1.put(BrowserContract.Tabs.LAST_USED, now);
remoteTab1.put(BrowserContract.Tabs.POSITION, 0);
final ContentValues remoteTab2 = new ContentValues();
remoteTab2.put(BrowserContract.Tabs.CLIENT_GUID, "guid1");
remoteTab2.put(BrowserContract.Tabs.TITLE, "title2");
remoteTab2.put(BrowserContract.Tabs.URL, "http://test.com/test2");
remoteTab2.put(BrowserContract.Tabs.HISTORY, "[\"http://test.com/test2\"]");
remoteTab2.put(BrowserContract.Tabs.LAST_USED, now + 5);
remoteTab2.put(BrowserContract.Tabs.POSITION, 1);
values = new ContentValues[]{remoteTab1, remoteTab2};
inserted = cpc.bulkInsert(BrowserContract.Tabs.CONTENT_URI, values);
assertEquals(2, inserted);
allClients = cpc.query(BrowserContract.Clients.CONTENT_RECENCY_URI, null, BrowserContract.Clients.GUID + " IS NOT NULL", null, null);
try {
CursorDumper.dumpCursor(allClients);
// The local client is ignored.
assertEquals(2, allClients.getCount());
final List<RemoteClient> clients = accessor.getClientsWithoutTabsByRecencyFromCursor(allClients);
assertEquals(2, clients.size());
for (RemoteClient client : clients) {
// Each client should be remote and should not have any tabs.
assertNotNull(client.guid);
assertNotNull(client.tabs);
assertEquals(0, client.tabs.size());
}
// Since now there is a tab attached to the remote2 client more recent than the
// remote1 client modified time, it should be first.
assertEquals("guid1", clients.get(0).guid);
assertEquals("guid2", clients.get(1).guid);
} finally {
allClients.close();
}
} finally {
cpc.release();
}
}
}

View File

@ -32,7 +32,7 @@ public class StringHelper {
public final String ABOUT_DOWNLOADS_URL = "about:downloads";
public final String ABOUT_HOME_URL = "about:home";
public final String ABOUT_ADDONS_URL = "about:addons";
public static final String ABOUT_PASSWORDS_URL = "about:passwords";
public static final String ABOUT_LOGINS_URL = "about:logins";
public final String ABOUT_APPS_URL = "about:apps";
public final String ABOUT_ABOUT_URL = "about:about";
public final String ABOUT_SCHEME = "about:";

View File

@ -6,7 +6,7 @@ subsuite = robocop
[testAboutPage.java]
# disabled on Android 2.3; bug 975187
skip-if = android_version == "10"
[testAboutPasswords.java]
[testAboutLogins.java]
[testAddonManager.java]
# disabled on 2.3: bug 941624, bug 1063509, bug 1073374, bug 1087221, bug 1088023, bug 1088027, bug 1090206; on 4.3, bug 1144918
skip-if = android_version == "10" || android_version == "18"

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