Merge fx-team to central, a=merge

This commit is contained in:
Wes Kocher 2016-06-08 17:06:18 -07:00
commit 07d01350a8
100 changed files with 16109 additions and 1315 deletions

View File

@ -114,6 +114,7 @@ devtools/server/**
devtools/shared/*.js
!devtools/shared/css-lexer.js
!devtools/shared/defer.js
!devtools/shared/event-emitter.js
!devtools/shared/task.js
devtools/shared/*.jsm
devtools/shared/apps/**
@ -136,6 +137,7 @@ devtools/shared/transport/**
!devtools/shared/transport/transport.js
devtools/shared/webconsole/test/**
devtools/shared/worker/**
!devtools/shared/worker/worker.js
# Ignore devtools pre-processed files
devtools/client/framework/toolbox-process-window.js

View File

@ -40,7 +40,7 @@ var gMenuBuilder = {
this.xulMenu = xulMenu;
for (let [, root] of gRootItems) {
let rootElement = this.buildElementWithChildren(root, contextData);
if (!rootElement.firstChild.childNodes.length) {
if (!rootElement.firstChild || !rootElement.firstChild.childNodes.length) {
// If the root has no visible children, there is no reason to show
// the root menu item itself either.
continue;

View File

@ -5,30 +5,31 @@
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
Cu.import("resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
"resource://devtools/shared/event-emitter.js");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyGetter(this, "History", () => {
Cu.import("resource://gre/modules/PlacesUtils.jsm");
return PlacesUtils.history;
});
Cu.import("resource://gre/modules/ExtensionUtils.jsm");
const {
normalizeTime,
SingletonEventManager,
} = ExtensionUtils;
let historySvc = Ci.nsINavHistoryService;
const History = PlacesUtils.history;
const TRANSITION_TO_TRANSITION_TYPES_MAP = new Map([
["link", historySvc.TRANSITION_LINK],
["typed", historySvc.TRANSITION_TYPED],
["auto_bookmark", historySvc.TRANSITION_BOOKMARK],
["auto_subframe", historySvc.TRANSITION_EMBED],
["manual_subframe", historySvc.TRANSITION_FRAMED_LINK],
["link", History.TRANSITION_LINK],
["typed", History.TRANSITION_TYPED],
["auto_bookmark", History.TRANSITION_BOOKMARK],
["auto_subframe", History.TRANSITION_EMBED],
["manual_subframe", History.TRANSITION_FRAMED_LINK],
]);
let TRANSITION_TYPE_TO_TRANSITIONS_MAP = new Map();
for (let [transition, transitionType] of TRANSITION_TO_TRANSITION_TYPES_MAP) {
TRANSITION_TYPE_TO_TRANSITIONS_MAP.set(transitionType, transition);
}
function getTransitionType(transition) {
// cannot set a default value for the transition argument as the framework sets it to null
transition = transition || "link";
@ -39,12 +40,16 @@ function getTransitionType(transition) {
return transitionType;
}
function getTransition(transitionType) {
return TRANSITION_TYPE_TO_TRANSITIONS_MAP.get(transitionType) || "link";
}
/*
* Converts a nsINavHistoryResultNode into a plain object
* Converts a nsINavHistoryResultNode into a HistoryItem
*
* https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryResultNode
*/
function convertNavHistoryResultNode(node) {
function convertNodeToHistoryItem(node) {
return {
id: node.pageGuid,
url: node.uri,
@ -54,17 +59,32 @@ function convertNavHistoryResultNode(node) {
};
}
/*
* Converts a nsINavHistoryResultNode into a VisitItem
*
* https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryResultNode
*/
function convertNodeToVisitItem(node) {
return {
id: node.pageGuid,
visitId: node.visitId,
visitTime: PlacesUtils.toDate(node.time).getTime(),
referringVisitId: node.fromVisitId,
transition: getTransition(node.visitType),
};
}
/*
* Converts a nsINavHistoryContainerResultNode into an array of objects
*
* https://developer.mozilla.org/en-US/docs/XPCOM_Interface_Reference/nsINavHistoryContainerResultNode
*/
function convertNavHistoryContainerResultNode(container) {
function convertNavHistoryContainerResultNode(container, converter) {
let results = [];
container.containerOpen = true;
for (let i = 0; i < container.childCount; i++) {
let node = container.getChild(i);
results.push(convertNavHistoryResultNode(node));
results.push(converter(node));
}
container.containerOpen = false;
return results;
@ -163,7 +183,23 @@ extensions.registerSchemaAPI("history", "history", (extension, context) => {
historyQuery.beginTime = beginTime;
historyQuery.endTime = endTime;
let queryResult = History.executeQuery(historyQuery, options).root;
let results = convertNavHistoryContainerResultNode(queryResult);
let results = convertNavHistoryContainerResultNode(queryResult, convertNodeToHistoryItem);
return Promise.resolve(results);
},
getVisits: function(details) {
let url = details.url;
if (!url) {
return Promise.reject({message: "A URL must be provided for getVisits"});
}
let options = History.getNewQueryOptions();
options.sortingMode = options.SORT_BY_DATE_DESCENDING;
options.resultType = options.RESULTS_AS_VISIT;
let historyQuery = History.getNewQuery();
historyQuery.uri = NetUtil.newURI(url);
let queryResult = History.executeQuery(historyQuery, options).root;
let results = convertNavHistoryContainerResultNode(queryResult, convertNodeToVisitItem);
return Promise.resolve(results);
},

View File

@ -155,7 +155,6 @@
},
{
"name": "getVisits",
"unsupported": true,
"type": "function",
"description": "Retrieves information about visits to a URL.",
"async": "callback",

View File

@ -295,3 +295,68 @@ add_task(function* test_add_url() {
yield extension.unload();
});
add_task(function* test_get_visits() {
function background() {
const TEST_DOMAIN = "http://example.com/";
const FIRST_DATE = Date.now();
const INITIAL_DETAILS = {
url: TEST_DOMAIN,
visitTime: FIRST_DATE,
transition: "link",
};
let visitIds = new Set();
function checkVisit(visit, expected) {
visitIds.add(visit.visitId);
browser.test.assertEq(expected.visitTime, visit.visitTime, "visit has the correct visitTime");
browser.test.assertEq(expected.transition, visit.transition, "visit has the correct transition");
browser.history.search({text: expected.url}).then(results => {
// all results will have the same id, so we only need to use the first one
browser.test.assertEq(results[0].id, visit.id, "visit has the correct id");
});
}
let details = Object.assign({}, INITIAL_DETAILS);
browser.history.addUrl(details).then(() => {
return browser.history.getVisits({url: details.url});
}).then(results => {
browser.test.assertEq(1, results.length, "the expected number of visits were returned");
checkVisit(results[0], details);
details.url = `${TEST_DOMAIN}/1/`;
return browser.history.addUrl(details);
}).then(() => {
return browser.history.getVisits({url: details.url});
}).then(results => {
browser.test.assertEq(1, results.length, "the expected number of visits were returned");
checkVisit(results[0], details);
details.visitTime = FIRST_DATE - 1000;
details.transition = "typed";
return browser.history.addUrl(details);
}).then(() => {
return browser.history.getVisits({url: details.url});
}).then(results => {
browser.test.assertEq(2, results.length, "the expected number of visits were returned");
checkVisit(results[0], INITIAL_DETAILS);
checkVisit(results[1], details);
}).then(() => {
browser.test.assertEq(3, visitIds.size, "each visit has a unique visitId");
browser.test.notifyPass("get-visits");
});
}
let extension = ExtensionTestUtils.loadExtension({
manifest: {
permissions: ["history"],
},
background: `(${background})()`,
});
yield PlacesTestUtils.clearHistory();
yield extension.startup();
yield extension.awaitFinish("get-visits");
yield extension.unload();
});

View File

@ -1240,7 +1240,7 @@ Toolbox.prototype = {
// backward compatibility with existing extensions do a check
// for a promise return value.
let built = definition.build(iframe.contentWindow, this);
if (!(built instanceof Promise)) {
if (!(typeof built.then == "function")) {
let panel = built;
iframe.panel = panel;

View File

@ -118,7 +118,7 @@ const Services = require("Services");
const {XPCOMUtils} = require("resource://gre/modules/XPCOMUtils.jsm");
const EventEmitter = require("devtools/shared/event-emitter");
const Editor = require("devtools/client/sourceeditor/editor");
const {TimelineFront} = require("devtools/server/actors/timeline");
const {TimelineFront} = require("devtools/shared/fronts/timeline");
const { Task } = require("devtools/shared/task");
XPCOMUtils.defineConstant(this, "EVENTS", EVENTS);

View File

@ -11,7 +11,7 @@ const { Poller } = require("devtools/client/shared/poller");
const CompatUtils = require("devtools/client/performance/legacy/compatibility");
const RecordingUtils = require("devtools/shared/performance/recording-utils");
const { TimelineFront } = require("devtools/server/actors/timeline");
const { TimelineFront } = require("devtools/shared/fronts/timeline");
const { ProfilerFront } = require("devtools/server/actors/profiler");
// how often do we check the status of the profiler's circular buffer

View File

@ -1431,7 +1431,7 @@ InplaceEditor.prototype = {
// Display the list of suggestions if there are more than one.
if (finalList.length > 1) {
// Calculate the popup horizontal offset.
let indent = this.input.selectionStart - query.length;
let indent = this.input.selectionStart - startCheckQuery.length;
let offset = indent * this.inputCharDimensions.width;
offset = this._isSingleLine() ? offset : 0;

View File

@ -124,6 +124,7 @@ skip-if = e10s # Bug 1221911, bug 1222289, frequent e10s timeouts
[browser_inplace-editor-02.js]
[browser_inplace-editor_autocomplete_01.js]
[browser_inplace-editor_autocomplete_02.js]
[browser_inplace-editor_autocomplete_offset.js]
[browser_inplace-editor_maxwidth.js]
[browser_key_shortcuts.js]
[browser_layoutHelpers.js]

View File

@ -13,6 +13,13 @@ loadHelperScript("helper_inplace_editor.js");
// Using a mocked list of CSS properties to avoid test failures linked to
// engine changes (new property, removed property, ...).
// format :
// [
// what key to press,
// expected input box value after keypress,
// selected suggestion index (-1 if popup is hidden),
// number of suggestions in the popup (0 if popup is hidden),
// ]
const testData = [
["b", "border", 1, 3],
["VK_DOWN", "box-sizing", 2, 3],

View File

@ -13,6 +13,13 @@ loadHelperScript("helper_inplace_editor.js");
// Using a mocked list of CSS properties to avoid test failures linked to
// engine changes (new property, removed property, ...).
// format :
// [
// what key to press,
// expected input box value after keypress,
// selected suggestion index (-1 if popup is hidden),
// number of suggestions in the popup (0 if popup is hidden),
// ]
const testData = [
["b", "block", -1, 0],
["VK_BACK_SPACE", "b", -1, 0],

View File

@ -0,0 +1,111 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/* import-globals-from helper_inplace_editor.js */
"use strict";
const { InplaceEditor } = require("devtools/client/shared/inplace-editor");
const { AutocompletePopup } = require("devtools/client/shared/autocomplete-popup");
loadHelperScript("helper_inplace_editor.js");
// Test the inplace-editor autocomplete popup is aligned with the completed query.
// Which means when completing "style=display:flex; color:" the popup will aim to be
// aligned with the ":" next to "color".
// format :
// [
// what key to press,
// expected input box value after keypress,
// selected suggestion index (-1 if popup is hidden),
// number of suggestions in the popup (0 if popup is hidden),
// ]
// or
// ["checkPopupOffset"]
// to measure and test the autocomplete popup left offset.
const testData = [
["VK_RIGHT", "style=", -1, 0],
["d", "style=display", 1, 2],
["checkPopupOffset"],
["VK_RIGHT", "style=display", -1, 0],
[":", "style=display:block", 0, 3],
["checkPopupOffset"],
["f", "style=display:flex", -1, 0],
["VK_RIGHT", "style=display:flex", -1, 0],
[";", "style=display:flex;", -1, 0],
["c", "style=display:flex;color", 1, 2],
["checkPopupOffset"],
["VK_RIGHT", "style=display:flex;color", -1, 0],
[":", "style=display:flex;color:blue", 0, 2],
["checkPopupOffset"],
];
const mockGetCSSPropertyList = function () {
return [
"clear",
"color",
"direction",
"display",
];
};
const mockGetCSSValuesForPropertyName = function (propertyName) {
let values = {
"color": ["blue", "red"],
"display": ["block", "flex", "none"]
};
return values[propertyName] || [];
};
add_task(function* () {
yield addTab("data:text/html;charset=utf-8,inplace editor CSS value autocomplete");
let [host, win, doc] = yield createHost();
let xulDocument = win.top.document;
let popup = new AutocompletePopup(xulDocument, { autoSelect: true });
info("Create a CSS_MIXED type autocomplete");
yield new Promise(resolve => {
createInplaceEditorAndClick({
initial: "style=",
start: runAutocompletionTest,
contentType: InplaceEditor.CONTENT_TYPES.CSS_MIXED,
done: resolve,
popup: popup
}, doc);
});
host.destroy();
gBrowser.removeCurrentTab();
});
let runAutocompletionTest = Task.async(function* (editor) {
info("Starting autocomplete test for inplace-editor popup offset");
editor._getCSSPropertyList = mockGetCSSPropertyList;
editor._getCSSValuesForPropertyName = mockGetCSSValuesForPropertyName;
let previousOffset = -1;
for (let data of testData) {
if (data[0] === "checkPopupOffset") {
info("Check the popup offset has been modified");
// We are not testing hard coded offset values here, which could be fragile. We only
// want to ensure the popup tries to match the position of the query in the editor
// input.
let offset = getPopupOffset(editor);
ok(offset > previousOffset, "New popup offset is greater than the previous one");
previousOffset = offset;
} else {
yield testCompletion(data, editor);
}
}
EventUtils.synthesizeKey("VK_RETURN", {}, editor.input.defaultView);
});
/**
* Get the autocomplete panel left offset, relative to the provided input's left offset.
*/
function getPopupOffset({popup, input}) {
let popupQuads = popup._panel.getBoxQuads({relativeTo: input});
return popupQuads[0].bounds.left;
}

View File

@ -42,7 +42,7 @@ add_task(function* () {
yield waitForMessages(msgForLocation1);
// load second url
content.location = TEST_URI2;
BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI2);
yield loadBrowser(gBrowser.selectedBrowser);
is(hud.outputNode.textContent.indexOf("Permission denied"), -1,

View File

@ -56,7 +56,7 @@ function loadDocument(browser) {
browser.removeEventListener("load", onLoad, true);
deferred.resolve();
}, true);
content.location = TEST_PATH;
BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_PATH);
return deferred.promise;
}

View File

@ -23,7 +23,7 @@ add_task(function* () {
expectUncaughtException();
}
content.location = TEST_URI;
BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI);
yield waitForMessages({
webconsole: hud,

View File

@ -28,7 +28,7 @@ add_task(function* () {
hud.jsterm.clearOutput();
let loaded = loadBrowser(browser);
content.location = uri.spec;
BrowserTestUtils.loadURI(gBrowser.selectedBrowser, uri.spec);
yield loaded;
yield testMessages();

View File

@ -179,7 +179,7 @@ function testNext() {
startNextTest();
}, true);
content.location = testLocation;
BrowserTestUtils.loadURI(gBrowser.selectedBrowser, testLocation);
} else {
testEnded = true;
finishTest();

View File

@ -37,7 +37,7 @@ function consoleOpened(hud) {
deferred.resolve();
}
};
content.location = TEST_URI2;
BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI2);
return deferred.promise;
}

View File

@ -33,7 +33,7 @@ add_task(function* () {
expectUncaughtException();
}
content.location = TEST_URI;
BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI);
yield loaded;
yield testWebDevLimits();

View File

@ -104,6 +104,6 @@ function test() {
waitForFocus(createDocument, content);
}, true);
content.location = "data:text/html;charset=utf-8,test for highlighter " +
"helper in web console";
BrowserTestUtils.loadURI(gBrowser.selectedBrowser,
"data:text/html;charset=utf-8,test for highlighter helper in web console");
}

View File

@ -30,7 +30,7 @@ add_task(function* () {
});
var testMixedContent = Task.async(function* (hud) {
content.location = TEST_HTTPS_URI;
BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_HTTPS_URI);
let results = yield waitForMessages({
webconsole: hud,

View File

@ -23,7 +23,7 @@ add_task(function* () {
hud.jsterm.clearOutput();
let loaded = loadBrowser(browser);
content.location = TEST_VIOLATION;
BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_VIOLATION);
yield loaded;
yield waitForSuccess({

View File

@ -50,10 +50,10 @@ function runTestLoop(theHud) {
if (gCurrentTest.pref) {
SpecialPowers.pushPrefEnv({"set": gCurrentTest.pref},
function () {
content.location = gCurrentTest.url;
BrowserTestUtils.loadURI(gBrowser.selectedBrowser, gCurrentTest.url);
});
} else {
content.location = gCurrentTest.url;
BrowserTestUtils.loadURI(gBrowser.selectedBrowser, gCurrentTest.url);
}
}

View File

@ -26,7 +26,7 @@ function test() {
const {tab} = yield loadTab("data:text/html;charset=utf8,<p>hello</p>");
const hud = yield openConsole(tab);
content.location = TEST_PAGE_URI;
BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_PAGE_URI);
let messages = yield waitForMessages({
webconsole: hud,

View File

@ -66,7 +66,7 @@ add_task(function* () {
function* checkForMessage(curTest, hud) {
hud.jsterm.clearOutput();
content.location = curTest.url;
BrowserTestUtils.loadURI(gBrowser.selectedBrowser, curTest.url);
let results = yield waitForMessages({
webconsole: hud,

View File

@ -26,7 +26,7 @@ add_task(function* () {
HUDService.lastFinishedRequest.callback = request => requests.push(request);
let loaded = loadBrowser(browser);
content.location = TEST_FILE_URI;
BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_FILE_URI);
yield loaded;
yield testMessages();

View File

@ -56,7 +56,7 @@ function loadDocument(browser) {
browser.removeEventListener("load", onLoad, true);
deferred.resolve();
}, true);
content.location = TEST_PATH;
BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_PATH);
return deferred.promise;
}

View File

@ -31,7 +31,8 @@ add_task(function* () {
if (!Services.appinfo.browserTabsRemoteAutostart) {
expectUncaughtException();
}
content.location = "data:text/html;charset=utf8,<script>'use strict';function f(a, a) {};</script>";
BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "data:text/html;charset="
+ "utf8,<script>'use strict';function f(a, a) {};</script>");
yield waitForMessages({
webconsole: hud,
@ -47,7 +48,8 @@ add_task(function* () {
if (!Services.appinfo.browserTabsRemoteAutostart) {
expectUncaughtException();
}
content.location = "data:text/html;charset=utf8,<script>'use strict';var o = {get p() {}};o.p = 1;</script>";
BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "data:text/html;charset="
+ "utf8,<script>'use strict';var o = {get p() {}};o.p = 1;</script>");
yield waitForMessages({
webconsole: hud,
@ -63,7 +65,8 @@ add_task(function* () {
if (!Services.appinfo.browserTabsRemoteAutostart) {
expectUncaughtException();
}
content.location = "data:text/html;charset=utf8,<script>'use strict';v = 1;</script>";
BrowserTestUtils.loadURI(gBrowser.selectedBrowser,
"data:text/html;charset=utf8,<script>'use strict';v = 1;</script>");
yield waitForMessages({
webconsole: hud,

View File

@ -508,3 +508,14 @@ function actorBridge(methodName, definition = {}) {
}, definition);
}
exports.actorBridge = actorBridge;
/**
* Like `actorBridge`, but without a spec definition, for when the actor is
* created with `ActorClassWithSpec` rather than vanilla `ActorClass`.
*/
function actorBridgeWithSpec (methodName) {
return method(function () {
return this.bridge[methodName].apply(this.bridge, arguments);
});
}
exports.actorBridgeWithSpec = actorBridgeWithSpec;

View File

@ -697,6 +697,7 @@ exports.SEL_ALL = [
/**
* Find a URL for a given stylesheet
* @param stylesheet {StyleSheet|StyleSheetActor}
*/
const sheetToUrl = exports.sheetToUrl = function (stylesheet) {
// For <link> elements
@ -712,5 +713,10 @@ const sheetToUrl = exports.sheetToUrl = function (stylesheet) {
return getURL(document) + " → <style> index " + index;
}
// When `stylesheet` is a StyleSheetActor, we don't have access to ownerNode
if (stylesheet.nodeHref) {
return stylesheet.nodeHref + " → <style> index " + stylesheet.styleSheetIndex;
}
throw new Error("Unknown sheet source");
};

View File

@ -17,86 +17,16 @@
*/
const protocol = require("devtools/shared/protocol");
const { method, Arg, RetVal, Option } = protocol;
const events = require("sdk/event/core");
const { Option, RetVal } = protocol;
const { actorBridgeWithSpec } = require("devtools/server/actors/common");
const { Timeline } = require("devtools/server/performance/timeline");
const { actorBridge } = require("devtools/server/actors/common");
/**
* Type representing an array of numbers as strings, serialized fast(er).
* http://jsperf.com/json-stringify-parse-vs-array-join-split/3
*
* XXX: It would be nice if on local connections (only), we could just *give*
* the array directly to the front, instead of going through all this
* serialization redundancy.
*/
protocol.types.addType("array-of-numbers-as-strings", {
write: (v) => v.join(","),
// In Gecko <= 37, `v` is an array; do not transform in this case.
read: (v) => typeof v === "string" ? v.split(",") : v
});
const { timelineSpec } = require("devtools/shared/specs/timeline");
const events = require("sdk/event/core");
/**
* The timeline actor pops and forwards timeline markers registered in docshells.
*/
var TimelineActor = exports.TimelineActor = protocol.ActorClass({
typeName: "timeline",
events: {
/**
* Events emitted when "DOMContentLoaded" and "Load" markers are received.
*/
"doc-loading" : {
type: "doc-loading",
marker: Arg(0, "json"),
endTime: Arg(0, "number")
},
/**
* The "markers" events emitted every DEFAULT_TIMELINE_DATA_PULL_TIMEOUT ms
* at most, when profile markers are found. The timestamps on each marker
* are relative to when recording was started.
*/
"markers" : {
type: "markers",
markers: Arg(0, "json"),
endTime: Arg(1, "number")
},
/**
* The "memory" events emitted in tandem with "markers", if this was enabled
* when the recording started. The `delta` timestamp on this measurement is
* relative to when recording was started.
*/
"memory" : {
type: "memory",
delta: Arg(0, "number"),
measurement: Arg(1, "json")
},
/**
* The "ticks" events (from the refresh driver) emitted in tandem with
* "markers", if this was enabled when the recording started. All ticks
* are timestamps with a zero epoch.
*/
"ticks" : {
type: "ticks",
delta: Arg(0, "number"),
timestamps: Arg(1, "array-of-numbers-as-strings")
},
/**
* The "frames" events emitted in tandem with "markers", containing
* JS stack frames. The `delta` timestamp on this frames packet is
* relative to when recording was started.
*/
"frames" : {
type: "frames",
delta: Arg(0, "number"),
frames: Arg(1, "json")
}
},
var TimelineActor = exports.TimelineActor = protocol.ActorClassWithSpec(timelineSpec, {
/**
* Initializes this actor with the provided connection and tab actor.
*/
@ -110,9 +40,9 @@ var TimelineActor = exports.TimelineActor = protocol.ActorClass({
},
/**
* The timeline actor is the first (and last) in its hierarchy to use protocol.js
* so it doesn't have a parent protocol actor that takes care of its lifetime.
* So it needs a disconnect method to cleanup.
* The timeline actor is the first (and last) in its hierarchy to use
* protocol.js so it doesn't have a parent protocol actor that takes care of
* its lifetime. So it needs a disconnect method to cleanup.
*/
disconnect: function () {
this.destroy();
@ -130,23 +60,21 @@ var TimelineActor = exports.TimelineActor = protocol.ActorClass({
},
/**
* Propagate events from the Timeline module over
* RDP if the event is defined here.
* Propagate events from the Timeline module over RDP if the event is defined
* here.
*/
_onTimelineEvent: function (eventName, ...args) {
if (this.events[eventName]) {
events.emit(this, eventName, ...args);
}
events.emit(this, eventName, ...args);
},
isRecording: actorBridge("isRecording", {
isRecording: actorBridgeWithSpec("isRecording", {
request: {},
response: {
value: RetVal("boolean")
}
}),
start: actorBridge("start", {
start: actorBridgeWithSpec("start", {
request: {
withMarkers: Option(0, "boolean"),
withTicks: Option(0, "boolean"),
@ -160,7 +88,7 @@ var TimelineActor = exports.TimelineActor = protocol.ActorClass({
}
}),
stop: actorBridge("stop", {
stop: actorBridgeWithSpec("stop", {
response: {
// Set as possibly nullable due to the end time possibly being
// undefined during destruction
@ -168,13 +96,3 @@ var TimelineActor = exports.TimelineActor = protocol.ActorClass({
}
}),
});
exports.TimelineFront = protocol.FrontClass(TimelineActor, {
initialize: function (client, {timelineActor}) {
protocol.Front.prototype.initialize.call(this, client, {actor: timelineActor});
this.manage(this);
},
destroy: function () {
protocol.Front.prototype.destroy.call(this);
},
});

View File

@ -5,7 +5,7 @@
* Test that we get DOMContentLoaded and Load markers
*/
const { TimelineFront } = require("devtools/server/actors/timeline");
const { TimelineFront } = require("devtools/shared/fronts/timeline");
const MARKER_NAMES = ["document::DOMContentLoaded", "document::Load"];
add_task(function* () {

View File

@ -5,7 +5,7 @@
* Test that we get DOMContentLoaded and Load markers
*/
const { TimelineFront } = require("devtools/server/actors/timeline");
const { TimelineFront } = require("devtools/shared/fronts/timeline");
const MARKER_NAMES = ["document::DOMContentLoaded", "document::Load"];
add_task(function* () {

View File

@ -5,7 +5,7 @@
* Test that we get DOMContentLoaded and Load markers
*/
const { TimelineFront } = require("devtools/server/actors/timeline");
const { TimelineFront } = require("devtools/shared/fronts/timeline");
const MARKER_NAMES = ["document::DOMContentLoaded", "document::Load"];
add_task(function* () {

View File

@ -10,7 +10,7 @@
// just that markers are recorded at all.
// Trying to check marker types here may lead to intermittents, see bug 1066474.
const {TimelineFront} = require("devtools/server/actors/timeline");
const {TimelineFront} = require("devtools/shared/fronts/timeline");
add_task(function* () {
let browser = yield addTab("data:text/html;charset=utf-8,mop");

View File

@ -7,7 +7,7 @@
// Test that the timeline can also record data from the memory and framerate
// actors, emitted as events in tadem with the markers.
const {TimelineFront} = require("devtools/server/actors/timeline");
const {TimelineFront} = require("devtools/shared/fronts/timeline");
add_task(function* () {
let browser = yield addTab("data:text/html;charset=utf-8,mop");

View File

@ -7,7 +7,7 @@
// Test the timeline front receives markers events for operations that occur in
// iframes.
const {TimelineFront} = require("devtools/server/actors/timeline");
const {TimelineFront} = require("devtools/shared/fronts/timeline");
add_task(function* () {
let browser = yield addTab(MAIN_DOMAIN + "timeline-iframe-parent.html");

View File

@ -2,18 +2,18 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* EventEmitter.
*/
"use strict";
(function (factory) { // Module boilerplate
if (this.module && module.id.indexOf("event-emitter") >= 0) { // require
(function (factory) {
if (this.module && module.id.indexOf("event-emitter") >= 0) {
// require
factory.call(this, require, exports, module);
} else { // Cu.import
} else {
// Cu.import
this.isWorker = false;
// Bug 1259045: This module is loaded early in firefox startup as a JSM,
// but it doesn't depends on any real module. We can save a few cycles
// and bytes by not loading Loader.jsm.
// Bug 1259045: This module is loaded early in firefox startup as a JSM,
// but it doesn't depends on any real module. We can save a few cycles
// and bytes by not loading Loader.jsm.
let require = function (module) {
const Cu = Components.utils;
switch (module) {
@ -33,15 +33,14 @@
this.EXPORTED_SYMBOLS = ["EventEmitter"];
}
}).call(this, function (require, exports, module) {
this.EventEmitter = function EventEmitter() {};
let EventEmitter = this.EventEmitter = function () {};
module.exports = EventEmitter;
// See comment in JSM module boilerplate when adding a new dependency.
const { Cu, components } = require("chrome");
// See comment in JSM module boilerplate when adding a new dependency.
const { components } = require("chrome");
const Services = require("Services");
const promise = require("promise");
var loggingEnabled = true;
let loggingEnabled = true;
if (!isWorker) {
loggingEnabled = Services.prefs.getBoolPref("devtools.dump.emit");
@ -52,127 +51,128 @@
}, false);
}
/**
* Decorate an object with event emitter functionality.
*
* @param Object aObjectToDecorate
* Bind all public methods of EventEmitter to
* the aObjectToDecorate object.
*/
EventEmitter.decorate = function EventEmitter_decorate(aObjectToDecorate) {
/**
* Decorate an object with event emitter functionality.
*
* @param Object objectToDecorate
* Bind all public methods of EventEmitter to
* the objectToDecorate object.
*/
EventEmitter.decorate = function (objectToDecorate) {
let emitter = new EventEmitter();
aObjectToDecorate.on = emitter.on.bind(emitter);
aObjectToDecorate.off = emitter.off.bind(emitter);
aObjectToDecorate.once = emitter.once.bind(emitter);
aObjectToDecorate.emit = emitter.emit.bind(emitter);
objectToDecorate.on = emitter.on.bind(emitter);
objectToDecorate.off = emitter.off.bind(emitter);
objectToDecorate.once = emitter.once.bind(emitter);
objectToDecorate.emit = emitter.emit.bind(emitter);
};
EventEmitter.prototype = {
/**
* Connect a listener.
*
* @param string aEvent
* The event name to which we're connecting.
* @param function aListener
* Called when the event is fired.
*/
on: function EventEmitter_on(aEvent, aListener) {
if (!this._eventEmitterListeners)
/**
* Connect a listener.
*
* @param string event
* The event name to which we're connecting.
* @param function listener
* Called when the event is fired.
*/
on(event, listener) {
if (!this._eventEmitterListeners) {
this._eventEmitterListeners = new Map();
if (!this._eventEmitterListeners.has(aEvent)) {
this._eventEmitterListeners.set(aEvent, []);
}
this._eventEmitterListeners.get(aEvent).push(aListener);
if (!this._eventEmitterListeners.has(event)) {
this._eventEmitterListeners.set(event, []);
}
this._eventEmitterListeners.get(event).push(listener);
},
/**
* Listen for the next time an event is fired.
*
* @param string aEvent
* The event name to which we're connecting.
* @param function aListener
* (Optional) Called when the event is fired. Will be called at most
* one time.
* @return promise
* A promise which is resolved when the event next happens. The
* resolution value of the promise is the first event argument. If
* you need access to second or subsequent event arguments (it's rare
* that this is needed) then use aListener
*/
once: function EventEmitter_once(aEvent, aListener) {
/**
* Listen for the next time an event is fired.
*
* @param string event
* The event name to which we're connecting.
* @param function listener
* (Optional) Called when the event is fired. Will be called at most
* one time.
* @return promise
* A promise which is resolved when the event next happens. The
* resolution value of the promise is the first event argument. If
* you need access to second or subsequent event arguments (it's rare
* that this is needed) then use listener
*/
once(event, listener) {
let deferred = promise.defer();
let handler = (aEvent, aFirstArg, ...aRest) => {
this.off(aEvent, handler);
if (aListener) {
aListener.apply(null, [aEvent, aFirstArg, ...aRest]);
let handler = (_, first, ...rest) => {
this.off(event, handler);
if (listener) {
listener.apply(null, [event, first, ...rest]);
}
deferred.resolve(aFirstArg);
deferred.resolve(first);
};
handler._originalListener = aListener;
this.on(aEvent, handler);
handler._originalListener = listener;
this.on(event, handler);
return deferred.promise;
},
/**
* Remove a previously-registered event listener. Works for events
* registered with either on or once.
*
* @param string aEvent
* The event name whose listener we're disconnecting.
* @param function aListener
* The listener to remove.
*/
off: function EventEmitter_off(aEvent, aListener) {
if (!this._eventEmitterListeners)
/**
* Remove a previously-registered event listener. Works for events
* registered with either on or once.
*
* @param string event
* The event name whose listener we're disconnecting.
* @param function listener
* The listener to remove.
*/
off(event, listener) {
if (!this._eventEmitterListeners) {
return;
let listeners = this._eventEmitterListeners.get(aEvent);
}
let listeners = this._eventEmitterListeners.get(event);
if (listeners) {
this._eventEmitterListeners.set(aEvent, listeners.filter(l => {
return l !== aListener && l._originalListener !== aListener;
this._eventEmitterListeners.set(event, listeners.filter(l => {
return l !== listener && l._originalListener !== listener;
}));
}
},
/**
* Emit an event. All arguments to this method will
* be sent to listener functions.
*/
emit: function EventEmitter_emit(aEvent) {
this.logEvent(aEvent, arguments);
/**
* Emit an event. All arguments to this method will
* be sent to listener functions.
*/
emit(event) {
this.logEvent(event, arguments);
if (!this._eventEmitterListeners || !this._eventEmitterListeners.has(aEvent)) {
if (!this._eventEmitterListeners || !this._eventEmitterListeners.has(event)) {
return;
}
let originalListeners = this._eventEmitterListeners.get(aEvent);
for (let listener of this._eventEmitterListeners.get(aEvent)) {
// If the object was destroyed during event emission, stop
// emitting.
let originalListeners = this._eventEmitterListeners.get(event);
for (let listener of this._eventEmitterListeners.get(event)) {
// If the object was destroyed during event emission, stop
// emitting.
if (!this._eventEmitterListeners) {
break;
}
// If listeners were removed during emission, make sure the
// event handler we're going to fire wasn't removed.
if (originalListeners === this._eventEmitterListeners.get(aEvent) ||
this._eventEmitterListeners.get(aEvent).some(l => l === listener)) {
// If listeners were removed during emission, make sure the
// event handler we're going to fire wasn't removed.
if (originalListeners === this._eventEmitterListeners.get(event) ||
this._eventEmitterListeners.get(event).some(l => l === listener)) {
try {
listener.apply(null, arguments);
} catch (ex) {
// Prevent a bad listener from interfering with the others.
let msg = ex + ": " + ex.stack;
console.error(msg);
dump(msg + "\n");
}
catch (ex) {
// Prevent a bad listener from interfering with the others.
let msg = ex + ": " + ex.stack;
console.error(msg);
dump(msg + "\n");
}
}
}
},
logEvent: function (aEvent, args) {
logEvent(event, args) {
if (!loggingEnabled) {
return;
}
@ -190,16 +190,16 @@
let argOut = "(";
if (args.length === 1) {
argOut += aEvent;
argOut += event;
}
let out = "EMITTING: ";
// We need this try / catch to prevent any dead object errors.
// We need this try / catch to prevent any dead object errors.
try {
for (let i = 1; i < args.length; i++) {
if (i === 1) {
argOut = "(" + aEvent + ", ";
argOut = "(" + event + ", ";
} else {
argOut += ", ";
}
@ -219,8 +219,8 @@
}
}
} catch (e) {
// Object is dead so the toolbox is most likely shutting down,
// do nothing.
// Object is dead so the toolbox is most likely shutting down,
// do nothing.
}
argOut += ")";
@ -229,5 +229,4 @@
dump(out);
},
};
});

View File

@ -24,6 +24,7 @@ DevToolsModules(
'string.js',
'styles.js',
'stylesheets.js',
'timeline.js',
'webaudio.js',
'webgl.js'
)

View File

@ -0,0 +1,25 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {
Front,
FrontClassWithSpec,
} = require("devtools/shared/protocol");
const { timelineSpec } = require("devtools/shared/specs/timeline");
/**
* TimelineFront, the front for the TimelineActor.
*/
const TimelineFront = FrontClassWithSpec(timelineSpec, {
initialize: function (client, { timelineActor }) {
Front.prototype.initialize.call(this, client, { actor: timelineActor });
this.manage(this);
},
destroy: function () {
Front.prototype.destroy.call(this);
},
});
exports.TimelineFront = TimelineFront;

View File

@ -32,6 +32,7 @@ DevToolsModules(
'styleeditor.js',
'styles.js',
'stylesheets.js',
'timeline.js',
'webaudio.js',
'webgl.js',
'worker.js'

View File

@ -0,0 +1,118 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {
Arg,
RetVal,
Option,
generateActorSpec,
types
} = require("devtools/shared/protocol");
/**
* Type representing an array of numbers as strings, serialized fast(er).
* http://jsperf.com/json-stringify-parse-vs-array-join-split/3
*
* XXX: It would be nice if on local connections (only), we could just *give*
* the array directly to the front, instead of going through all this
* serialization redundancy.
*/
types.addType("array-of-numbers-as-strings", {
write: (v) => v.join(","),
// In Gecko <= 37, `v` is an array; do not transform in this case.
read: (v) => typeof v === "string" ? v.split(",") : v
});
const timelineSpec = generateActorSpec({
typeName: "timeline",
events: {
/**
* Events emitted when "DOMContentLoaded" and "Load" markers are received.
*/
"doc-loading": {
type: "doc-loading",
marker: Arg(0, "json"),
endTime: Arg(0, "number")
},
/**
* The "markers" events emitted every DEFAULT_TIMELINE_DATA_PULL_TIMEOUT ms
* at most, when profile markers are found. The timestamps on each marker
* are relative to when recording was started.
*/
"markers": {
type: "markers",
markers: Arg(0, "json"),
endTime: Arg(1, "number")
},
/**
* The "memory" events emitted in tandem with "markers", if this was enabled
* when the recording started. The `delta` timestamp on this measurement is
* relative to when recording was started.
*/
"memory": {
type: "memory",
delta: Arg(0, "number"),
measurement: Arg(1, "json")
},
/**
* The "ticks" events (from the refresh driver) emitted in tandem with
* "markers", if this was enabled when the recording started. All ticks
* are timestamps with a zero epoch.
*/
"ticks": {
type: "ticks",
delta: Arg(0, "number"),
timestamps: Arg(1, "array-of-numbers-as-strings")
},
/**
* The "frames" events emitted in tandem with "markers", containing
* JS stack frames. The `delta` timestamp on this frames packet is
* relative to when recording was started.
*/
"frames": {
type: "frames",
delta: Arg(0, "number"),
frames: Arg(1, "json")
}
},
methods: {
isRecording: {
request: {},
response: {
value: RetVal("boolean")
}
},
start: {
request: {
withMarkers: Option(0, "boolean"),
withTicks: Option(0, "boolean"),
withMemory: Option(0, "boolean"),
withFrames: Option(0, "boolean"),
withGCEvents: Option(0, "boolean"),
withDocLoadingEvents: Option(0, "boolean")
},
response: {
value: RetVal("number")
}
},
stop: {
response: {
// Set as possibly nullable due to the end time possibly being
// undefined during destruction
value: RetVal("nullable:number")
}
},
},
});
exports.timelineSpec = timelineSpec;

View File

@ -1,14 +1,19 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
(function (factory) { // Module boilerplate
if (this.module && module.id.indexOf("worker") >= 0) { // require
/* global ChromeWorker */
(function (factory) {
if (this.module && module.id.indexOf("worker") >= 0) {
// require
const { Cc, Ci, Cu, ChromeWorker } = require("chrome");
const dumpn = require("devtools/shared/DevToolsUtils").dumpn;
factory.call(this, require, exports, module, { Cc, Ci, Cu }, ChromeWorker, dumpn);
} else { // Cu.import
} else {
// Cu.import
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
this.isWorker = false;
@ -21,23 +26,22 @@
this.EXPORTED_SYMBOLS = ["DevToolsWorker"];
}
}).call(this, function (require, exports, module, { Ci, Cc }, ChromeWorker, dumpn) {
let MESSAGE_COUNTER = 0;
var MESSAGE_COUNTER = 0;
/**
* Creates a wrapper around a ChromeWorker, providing easy
* communication to offload demanding tasks. The corresponding URL
* must implement the interface provided by `devtools/shared/worker/helper`.
*
* @see `./devtools/client/shared/widgets/GraphsWorker.js`
*
* @param {string} url
* The URL of the worker.
* @param Object opts
* An option with the following optional fields:
* - name: a name that will be printed with logs
* - verbose: log incoming and outgoing messages
*/
/**
* Creates a wrapper around a ChromeWorker, providing easy
* communication to offload demanding tasks. The corresponding URL
* must implement the interface provided by `devtools/shared/worker/helper`.
*
* @see `./devtools/client/shared/widgets/GraphsWorker.js`
*
* @param {string} url
* The URL of the worker.
* @param Object opts
* An option with the following optional fields:
* - name: a name that will be printed with logs
* - verbose: log incoming and outgoing messages
*/
function DevToolsWorker(url, opts) {
opts = opts || {};
this._worker = new ChromeWorker(url);
@ -48,17 +52,17 @@
}
exports.DevToolsWorker = DevToolsWorker;
/**
* Performs the given task in a chrome worker, passing in data.
* Returns a promise that resolves when the task is completed, resulting in
* the return value of the task.
*
* @param {string} task
* The name of the task to execute in the worker.
* @param {any} data
* Data to be passed into the task implemented by the worker.
* @return {Promise}
*/
/**
* Performs the given task in a chrome worker, passing in data.
* Returns a promise that resolves when the task is completed, resulting in
* the return value of the task.
*
* @param {string} task
* The name of the task to execute in the worker.
* @param {any} data
* Data to be passed into the task implemented by the worker.
* @return {Promise}
*/
DevToolsWorker.prototype.performTask = function (task, data) {
if (this._destroyed) {
return Promise.reject("Cannot call performTask on a destroyed DevToolsWorker");
@ -69,29 +73,29 @@
if (this._verbose && dumpn) {
dumpn("Sending message to worker" +
(this._name ? (" (" + this._name + ")") : "") +
": " +
JSON.stringify(payload, null, 2));
(this._name ? (" (" + this._name + ")") : "") +
": " +
JSON.stringify(payload, null, 2));
}
worker.postMessage(payload);
return new Promise((resolve, reject) => {
let listener = ({ data }) => {
let listener = ({ data: result }) => {
if (this._verbose && dumpn) {
dumpn("Received message from worker" +
(this._name ? (" (" + this._name + ")") : "") +
": " +
JSON.stringify(data, null, 2));
(this._name ? (" (" + this._name + ")") : "") +
": " +
JSON.stringify(result, null, 2));
}
if (data.id !== id) {
if (result.id !== id) {
return;
}
worker.removeEventListener("message", listener);
if (data.error) {
reject(data.error);
if (result.error) {
reject(result.error);
} else {
resolve(data.response);
resolve(result.response);
}
};
@ -99,9 +103,9 @@
});
};
/**
* Terminates the underlying worker. Use when no longer needing the worker.
*/
/**
* Terminates the underlying worker. Use when no longer needing the worker.
*/
DevToolsWorker.prototype.destroy = function () {
this._worker.terminate();
this._worker = null;
@ -112,30 +116,31 @@
dump(new Error(message + " @ " + filename + ":" + lineno) + "\n");
};
/**
* Takes a function and returns a Worker-wrapped version of the same function.
* Returns a promise upon resolution.
* @see `./devtools/shared/shared/tests/browser/browser_devtools-worker-03.js
*
* * * * ! ! ! This should only be used for tests or A/B testing performance ! ! ! * * * * * *
*
* The original function must:
*
* Be a pure function, that is, not use any variables not declared within the
* function, or its arguments.
*
* Return a value or a promise.
*
* Note any state change in the worker will not affect the callee's context.
*
* @param {function} fn
* @return {function}
*/
/**
* Takes a function and returns a Worker-wrapped version of the same function.
* Returns a promise upon resolution.
* @see `./devtools/shared/shared/tests/browser/browser_devtools-worker-03.js
*
* This should only be used for tests or A/B testing performance
*
* The original function must:
*
* Be a pure function, that is, not use any variables not declared within the
* function, or its arguments.
*
* Return a value or a promise.
*
* Note any state change in the worker will not affect the callee's context.
*
* @param {function} fn
* @return {function}
*/
function workerify(fn) {
console.warn(`\`workerify\` should only be used in tests or measuring performance.
This creates an object URL on the browser window, and should not be used in production.`);
// Fetch via window/utils here as we don't want to include
// this module normally.
console.warn("`workerify` should only be used in tests or measuring performance. " +
"This creates an object URL on the browser window, and should not be " +
"used in production.");
// Fetch via window/utils here as we don't want to include
// this module normally.
let { getMostRecentBrowserWindow } = require("sdk/window/utils");
let { URL, Blob } = getMostRecentBrowserWindow();
let stringifiedFn = createWorkerString(fn);
@ -154,15 +159,13 @@
}
exports.workerify = workerify;
/**
* Takes a function, and stringifies it, attaching the worker-helper.js
* boilerplate hooks.
*/
/**
* Takes a function, and stringifies it, attaching the worker-helper.js
* boilerplate hooks.
*/
function createWorkerString(fn) {
return `importScripts("resource://gre/modules/workers/require.js");
const { createTask } = require("resource://devtools/shared/worker/helper.js");
createTask(self, "workerifiedTask", ${fn.toString()});
`;
const { createTask } = require("resource://devtools/shared/worker/helper.js");
createTask(self, "workerifiedTask", ${fn.toString()});`;
}
});

View File

@ -141,7 +141,7 @@ pref("browser.sessionhistory.bfcacheIgnoreMemoryPressure", false);
pref("browser.sessionstore.resume_session_once", false);
pref("browser.sessionstore.resume_from_crash", true);
pref("browser.sessionstore.interval", 10000); // milliseconds
pref("browser.sessionstore.max_tabs_undo", 5);
pref("browser.sessionstore.max_tabs_undo", 10);
pref("browser.sessionstore.max_resumed_crashes", 1);
pref("browser.sessionstore.privacy_level", 0); // saving data: 0 = all, 1 = unencrypted sites, 2 = never
pref("browser.sessionstore.debug_logging", false);

View File

@ -10,6 +10,7 @@ import android.app.DownloadManager;
import android.os.Environment;
import android.support.annotation.CheckResult;
import android.support.annotation.NonNull;
import org.json.JSONArray;
import org.mozilla.gecko.adjust.AdjustHelperInterface;
import org.mozilla.gecko.annotation.RobocopTarget;
@ -2528,6 +2529,12 @@ public class BrowserApp extends GeckoApp
// history, and loading new pages, see Bug 1268887
panelId = tab.getMostRecentHomePanel();
panelRestoreData = tab.getMostRecentHomePanelData();
} else if (panelId.equals(HomeConfig.getIdForBuiltinPanelType(PanelType.DEPRECATED_RECENT_TABS))) {
// Redirect to the Combined History panel.
panelId = HomeConfig.getIdForBuiltinPanelType(PanelType.COMBINED_HISTORY);
panelRestoreData = new Bundle();
// Jump directly to the Recent Tabs subview of the Combined History panel.
panelRestoreData.putBoolean("goToRecentTabs", true);
}
showHomePager(panelId, panelRestoreData);

View File

@ -1453,14 +1453,17 @@ public abstract class GeckoApp
/**
* Loads the initial tab at Fennec startup. If we don't restore tabs, this
* tab will be about:home, or the homepage if the user has set one.
* If we've temporarily disabled restoring to break out of a crash loop, we'll
* show the recent tabs panel so the user can manually restore tabs as needed.
* If we've temporarily disabled restoring to break out of a crash loop, we'll show
* the Recent Tabs folder of the Combined History panel, so the user can manually
* restore tabs as needed.
* If we restore tabs, we don't need to create a new tab.
*/
protected void loadStartupTab(final int flags) {
if (!mShouldRestore) {
if (mLastSessionCrashed) {
Tabs.getInstance().loadUrl(AboutPages.getURLForBuiltinPanelType(PanelType.RECENT_TABS), flags);
// The Recent Tabs panel no longer exists, but BrowserApp will redirect us
// to the Recent Tabs folder of the Combined History panel.
Tabs.getInstance().loadUrl(AboutPages.getURLForBuiltinPanelType(PanelType.DEPRECATED_RECENT_TABS), flags);
} else {
final String homepage = getHomepage();
Tabs.getInstance().loadUrl(!TextUtils.isEmpty(homepage) ? homepage : AboutPages.HOME, flags);

View File

@ -145,6 +145,10 @@ public class ClientsAdapter extends RecyclerView.Adapter<CombinedHistoryItem> im
return CombinedHistoryItem.ItemType.itemTypeToViewType(getItemTypeForPosition(position));
}
public int getClientsCount() {
return hiddenClients.size() + visibleClients.size();
}
@UiThread
public void setClients(List<RemoteClient> clients) {
adapterList.clear();

View File

@ -17,7 +17,8 @@ import org.mozilla.gecko.R;
import org.mozilla.gecko.db.BrowserContract;
public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistoryItem> implements CombinedHistoryRecyclerView.AdapterContextMenuBuilder {
private static final int SYNCED_DEVICES_SMARTFOLDER_INDEX = 0;
private static final int RECENT_TABS_SMARTFOLDER_INDEX = 0;
private static final int SYNCED_DEVICES_SMARTFOLDER_INDEX = 1;
// Array for the time ranges in milliseconds covered by each section.
static final HistorySectionsHelper.SectionDateRange[] sectionDateRangeArray = new HistorySectionsHelper.SectionDateRange[SectionHeader.values().length];
@ -39,6 +40,8 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
private Cursor historyCursor;
private DevicesUpdateHandler devicesUpdateHandler;
private int deviceCount = 0;
private RecentTabsUpdateHandler recentTabsUpdateHandler;
private int recentTabsCount = 0;
// We use a sparse array to store each section header's position in the panel [more cheaply than a HashMap].
private final SparseArray<SectionHeader> sectionHeaders;
@ -65,13 +68,30 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
@Override
public void onDeviceCountUpdated(int count) {
deviceCount = count;
notifyItemChanged(0);
notifyItemChanged(SYNCED_DEVICES_SMARTFOLDER_INDEX);
}
};
}
return devicesUpdateHandler;
}
public interface RecentTabsUpdateHandler {
void onRecentTabsCountUpdated(int count);
}
public RecentTabsUpdateHandler getRecentTabsUpdateHandler() {
if (recentTabsUpdateHandler == null) {
recentTabsUpdateHandler = new RecentTabsUpdateHandler() {
@Override
public void onRecentTabsCountUpdated(int count) {
recentTabsCount = count;
notifyItemChanged(RECENT_TABS_SMARTFOLDER_INDEX);
}
};
}
return recentTabsUpdateHandler;
}
@Override
public CombinedHistoryItem onCreateViewHolder(ViewGroup viewGroup, int viewType) {
final LayoutInflater inflater = LayoutInflater.from(viewGroup.getContext());
@ -80,6 +100,7 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType);
switch (itemType) {
case RECENT_TABS:
case SYNCED_DEVICES:
view = inflater.inflate(R.layout.home_smartfolder, viewGroup, false);
return new CombinedHistoryItem.SmartFolder(view);
@ -102,6 +123,10 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
final int localPosition = transformAdapterPositionForDataStructure(itemType, position);
switch (itemType) {
case RECENT_TABS:
((CombinedHistoryItem.SmartFolder) viewHolder).bind(R.drawable.icon_recent, R.string.home_closed_tabs_title2, R.string.home_closed_tabs_one, R.string.home_closed_tabs_number, recentTabsCount);
break;
case SYNCED_DEVICES:
((CombinedHistoryItem.SmartFolder) viewHolder).bind(R.drawable.cloud, R.string.home_synced_devices_smartfolder, R.string.home_synced_devices_one, R.string.home_synced_devices_number, deviceCount);
break;
@ -140,6 +165,9 @@ public class CombinedHistoryAdapter extends RecyclerView.Adapter<CombinedHistory
}
private CombinedHistoryItem.ItemType getItemTypeForPosition(int position) {
if (position == RECENT_TABS_SMARTFOLDER_INDEX) {
return CombinedHistoryItem.ItemType.RECENT_TABS;
}
if (position == SYNCED_DEVICES_SMARTFOLDER_INDEX) {
return CombinedHistoryItem.ItemType.SYNCED_DEVICES;
}

View File

@ -15,6 +15,7 @@ import android.widget.TextView;
import org.mozilla.gecko.R;
import org.mozilla.gecko.db.RemoteClient;
import org.mozilla.gecko.db.RemoteTab;
import org.mozilla.gecko.home.RecentTabsAdapter.ClosedTab;
public abstract class CombinedHistoryItem extends RecyclerView.ViewHolder {
private static final String LOGTAG = "CombinedHistoryItem";
@ -24,7 +25,8 @@ public abstract class CombinedHistoryItem extends RecyclerView.ViewHolder {
}
public enum ItemType {
CLIENT, HIDDEN_DEVICES, SECTION_HEADER, HISTORY, NAVIGATION_BACK, CHILD, SYNCED_DEVICES;
CLIENT, HIDDEN_DEVICES, SECTION_HEADER, HISTORY, NAVIGATION_BACK, CHILD, SYNCED_DEVICES,
RECENT_TABS, CLOSED_TAB;
public static ItemType viewTypeToItemType(int viewType) {
if (viewType >= ItemType.values().length) {
@ -83,6 +85,12 @@ public abstract class CombinedHistoryItem extends RecyclerView.ViewHolder {
childPageRow.setShowIcons(true);
childPageRow.update(remoteTab.title, remoteTab.url);
}
public void bind(ClosedTab closedTab) {
final TwoLinePageRow childPageRow = (TwoLinePageRow) this.itemView;
childPageRow.setShowIcons(false);
childPageRow.update(closedTab.title, closedTab.url);
}
}
public static class ClientItem extends CombinedHistoryItem {

View File

@ -44,6 +44,7 @@ import org.mozilla.gecko.RemoteClientsDialogFragment;
import org.mozilla.gecko.fxa.FirefoxAccounts;
import org.mozilla.gecko.fxa.FxAccountConstants;
import org.mozilla.gecko.fxa.SyncStatusListener;
import org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel;
import org.mozilla.gecko.restrictions.Restrictions;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
@ -68,16 +69,21 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
private final static String FORMAT_S2 = "%2$s";
// Number of smart folders for determining practical empty state.
public static final int NUM_SMART_FOLDERS = 1;
public static final int NUM_SMART_FOLDERS = 2;
private CombinedHistoryRecyclerView mRecyclerView;
private CombinedHistoryAdapter mHistoryAdapter;
private ClientsAdapter mClientsAdapter;
private RecentTabsAdapter mRecentTabsAdapter;
private CursorLoaderCallbacks mCursorLoaderCallbacks;
private OnPanelLevelChangeListener.PanelLevel mPanelLevel;
private Bundle mSavedRestoreBundle;
private PanelLevel mPanelLevel;
private Button mPanelFooterButton;
private PanelStateUpdateHandler mPanelStateUpdateHandler;
// Child refresh layout view.
protected SwipeRefreshLayout mRefreshLayout;
@ -87,10 +93,11 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
// Reference to the View to display when there are no results.
private View mHistoryEmptyView;
private View mClientsEmptyView;
private View mRecentTabsEmptyView;
public interface OnPanelLevelChangeListener {
enum PanelLevel {
PARENT, CHILD
PARENT, CHILD_SYNC, CHILD_RECENT_TABS
}
/**
@ -107,6 +114,11 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
mHistoryAdapter = new CombinedHistoryAdapter(getResources());
mClientsAdapter = new ClientsAdapter(getContext());
// The RecentTabsAdapter doesn't use a cursor and therefore can't use the CursorLoader's
// onLoadFinished() callback for updating the panel state when the closed tab count changes.
// Instead, we provide it with independent callbacks as necessary.
mRecentTabsAdapter = new RecentTabsAdapter(getContext(),
mHistoryAdapter.getRecentTabsUpdateHandler(), getPanelStateUpdateHandler());
mSyncStatusListener = new RemoteTabsSyncListener();
FirefoxAccounts.addSyncStatusListener(mSyncStatusListener);
@ -129,18 +141,28 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
mClientsEmptyView = view.findViewById(R.id.home_clients_empty_view);
mHistoryEmptyView = view.findViewById(R.id.home_history_empty_view);
mRecentTabsEmptyView = view.findViewById(R.id.home_recent_tabs_empty_view);
setUpEmptyViews();
mPanelFooterButton = (Button) view.findViewById(R.id.clear_history_button);
mPanelFooterButton = (Button) view.findViewById(R.id.history_panel_footer_button);
mPanelFooterButton.setText(R.string.home_clear_history_button);
mPanelFooterButton.setOnClickListener(new OnFooterButtonClickListener());
mRecentTabsAdapter.startListeningForClosedTabs();
if (mSavedRestoreBundle != null) {
setPanelStateFromBundle(mSavedRestoreBundle);
mSavedRestoreBundle = null;
}
}
private void setUpRecyclerView() {
if (mPanelLevel == null) {
mPanelLevel = OnPanelLevelChangeListener.PanelLevel.PARENT;
mPanelLevel = PanelLevel.PARENT;
}
mRecyclerView.setAdapter(mPanelLevel == OnPanelLevelChangeListener.PanelLevel.PARENT ? mHistoryAdapter : mClientsAdapter);
mRecyclerView.setAdapter(mPanelLevel == PanelLevel.PARENT ? mHistoryAdapter :
mPanelLevel == PanelLevel.CHILD_SYNC ? mClientsAdapter : mRecentTabsAdapter);
final RecyclerView.ItemAnimator animator = new DefaultItemAnimator();
animator.setAddDuration(100);
@ -158,7 +180,7 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
final LinearLayoutManager llm = (LinearLayoutManager) recyclerView.getLayoutManager();
if ((mPanelLevel == OnPanelLevelChangeListener.PanelLevel.PARENT) && (llm.findLastCompletelyVisibleItemPosition() == HistoryCursorLoader.HISTORY_LIMIT)) {
if ((mPanelLevel == PanelLevel.PARENT) && (llm.findLastCompletelyVisibleItemPosition() == HistoryCursorLoader.HISTORY_LIMIT)) {
Telemetry.sendUIEvent(TelemetryContract.Event.ACTION, TelemetryContract.Method.LIST, "history_scroll_max");
}
@ -170,27 +192,28 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
private void setUpRefreshLayout() {
mRefreshLayout.setColorSchemeResources(R.color.fennec_ui_orange, R.color.action_orange);
mRefreshLayout.setOnRefreshListener(new RemoteTabsRefreshListener());
mRefreshLayout.setEnabled(false);
}
private void setUpEmptyViews() {
// Set up history empty view.
final ImageView emptyIcon = (ImageView) mHistoryEmptyView.findViewById(R.id.home_empty_image);
emptyIcon.setVisibility(View.GONE);
final ImageView historyIcon = (ImageView) mHistoryEmptyView.findViewById(R.id.home_empty_image);
historyIcon.setVisibility(View.GONE);
final TextView emptyText = (TextView) mHistoryEmptyView.findViewById(R.id.home_empty_text);
emptyText.setText(R.string.home_most_recent_empty);
final TextView historyText = (TextView) mHistoryEmptyView.findViewById(R.id.home_empty_text);
historyText.setText(R.string.home_most_recent_empty);
final TextView emptyHint = (TextView) mHistoryEmptyView.findViewById(R.id.home_empty_hint);
final TextView historyHint = (TextView) mHistoryEmptyView.findViewById(R.id.home_empty_hint);
if (!Restrictions.isAllowed(getActivity(), Restrictable.PRIVATE_BROWSING)) {
emptyHint.setVisibility(View.GONE);
historyHint.setVisibility(View.GONE);
} else {
final String hintText = getResources().getString(R.string.home_most_recent_emptyhint);
final SpannableStringBuilder hintBuilder = formatHintText(hintText);
if (hintBuilder != null) {
emptyHint.setText(hintBuilder);
emptyHint.setMovementMethod(LinkMovementMethod.getInstance());
emptyHint.setVisibility(View.VISIBLE);
historyHint.setText(hintBuilder);
historyHint.setMovementMethod(LinkMovementMethod.getInstance());
historyHint.setVisibility(View.VISIBLE);
}
}
@ -205,6 +228,31 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
startActivity(intent);
}
});
// Set up Recent Tabs empty view.
final ImageView recentTabsIcon = (ImageView) mRecentTabsEmptyView.findViewById(R.id.home_empty_image);
recentTabsIcon.setImageResource(R.drawable.icon_remote_tabs_empty);
final TextView recentTabsText = (TextView) mRecentTabsEmptyView.findViewById(R.id.home_empty_text);
recentTabsText.setText(R.string.home_last_tabs_empty);
}
@Override
public void restoreData(Bundle data) {
if (mRecyclerView != null) {
setPanelStateFromBundle(data);
} else {
mSavedRestoreBundle = data;
}
}
private void setPanelStateFromBundle(Bundle data) {
if (data != null && data.getBoolean("goToRecentTabs", false) && mPanelLevel != PanelLevel.CHILD_RECENT_TABS) {
mPanelLevel = PanelLevel.CHILD_RECENT_TABS;
mRecyclerView.swapAdapter(mRecentTabsAdapter, true);
updateEmptyView();
updateButtonFromLevel();
}
}
@Override
@ -282,7 +330,6 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
final List<RemoteClient> clients = mDB.getTabsAccessor().getClientsFromCursor(c);
mHistoryAdapter.getDeviceUpdateHandler().onDeviceCountUpdated(clients.size());
mClientsAdapter.setClients(clients);
mRefreshLayout.setEnabled(clients.size() > 0);
break;
}
@ -297,6 +344,23 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
}
}
public interface PanelStateUpdateHandler {
void onPanelStateUpdated();
}
public PanelStateUpdateHandler getPanelStateUpdateHandler() {
if (mPanelStateUpdateHandler == null) {
mPanelStateUpdateHandler = new PanelStateUpdateHandler() {
@Override
public void onPanelStateUpdated() {
updateEmptyView();
updateButtonFromLevel();
}
};
}
return mPanelStateUpdateHandler;
}
protected class OnLevelChangeListener implements OnPanelLevelChangeListener {
@Override
public boolean changeLevel(PanelLevel level) {
@ -308,9 +372,14 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
switch (level) {
case PARENT:
mRecyclerView.swapAdapter(mHistoryAdapter, true);
mRefreshLayout.setEnabled(false);
break;
case CHILD:
case CHILD_SYNC:
mRecyclerView.swapAdapter(mClientsAdapter, true);
mRefreshLayout.setEnabled(mClientsAdapter.getClientsCount() > 0);
break;
case CHILD_RECENT_TABS:
mRecyclerView.swapAdapter(mRecentTabsAdapter, true);
break;
}
@ -327,10 +396,19 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
if (historyRestricted || mHistoryAdapter.getItemCount() == NUM_SMART_FOLDERS) {
mPanelFooterButton.setVisibility(View.GONE);
} else {
mPanelFooterButton.setText(R.string.home_clear_history_button);
mPanelFooterButton.setVisibility(View.VISIBLE);
}
break;
case CHILD:
case CHILD_RECENT_TABS:
if (mRecentTabsAdapter.getClosedTabsCount() > 1) {
mPanelFooterButton.setText(R.string.home_restore_all);
mPanelFooterButton.setVisibility(View.VISIBLE);
} else {
mPanelFooterButton.setVisibility(View.GONE);
}
break;
case CHILD_SYNC:
mPanelFooterButton.setVisibility(View.GONE);
break;
}
@ -339,56 +417,72 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
private class OnFooterButtonClickListener implements View.OnClickListener {
@Override
public void onClick(View view) {
switch (mPanelLevel) {
case PARENT:
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity());
dialogBuilder.setMessage(R.string.home_clear_history_confirm);
dialogBuilder.setNegativeButton(R.string.button_cancel, new AlertDialog.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
dialog.dismiss();
}
});
final AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity());
dialogBuilder.setMessage(R.string.home_clear_history_confirm);
dialogBuilder.setNegativeButton(R.string.button_cancel, new AlertDialog.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
dialog.dismiss();
}
});
dialogBuilder.setPositiveButton(R.string.button_ok, new AlertDialog.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
dialog.dismiss();
dialogBuilder.setPositiveButton(R.string.button_ok, new AlertDialog.OnClickListener() {
@Override
public void onClick(final DialogInterface dialog, final int which) {
dialog.dismiss();
// Send message to Java to clear history.
final JSONObject json = new JSONObject();
try {
json.put("history", true);
} catch (JSONException e) {
Log.e(LOGTAG, "JSON error", e);
}
// Send message to Java to clear history.
final JSONObject json = new JSONObject();
try {
json.put("history", true);
} catch (JSONException e) {
Log.e(LOGTAG, "JSON error", e);
GeckoAppShell.notifyObservers("Sanitize:ClearData", json.toString());
mRecentTabsAdapter.clearLastSessionData();
Telemetry.sendUIEvent(TelemetryContract.Event.SANITIZE, TelemetryContract.Method.BUTTON, "history");
}
});
dialogBuilder.show();
break;
case CHILD_RECENT_TABS:
final String telemetryExtra = mRecentTabsAdapter.restoreAllTabs();
if (telemetryExtra != null) {
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.BUTTON, telemetryExtra);
}
GeckoAppShell.notifyObservers("Sanitize:ClearData", json.toString());
Telemetry.sendUIEvent(TelemetryContract.Event.SANITIZE, TelemetryContract.Method.BUTTON, "history");
}
});
dialogBuilder.show();
break;
}
}
}
private void updateEmptyView() {
boolean showEmptyHistoryView = false;
boolean showEmptyClientsView = false;
boolean showEmptyRecentTabsView = false;
switch (mPanelLevel) {
case PARENT:
showEmptyHistoryView = mHistoryAdapter.getItemCount() == NUM_SMART_FOLDERS;
break;
case CHILD:
case CHILD_SYNC:
showEmptyClientsView = mClientsAdapter.getItemCount() == 1;
break;
case CHILD_RECENT_TABS:
showEmptyRecentTabsView = mRecentTabsAdapter.getClosedTabsCount() == 0;
break;
}
final boolean showEmptyView = showEmptyClientsView || showEmptyHistoryView;
final boolean showEmptyView = showEmptyClientsView || showEmptyHistoryView || showEmptyRecentTabsView;
mRecyclerView.setOverScrollMode(showEmptyView ? View.OVER_SCROLL_NEVER : View.OVER_SCROLL_IF_CONTENT_SCROLLS);
mClientsEmptyView.setVisibility(showEmptyClientsView ? View.VISIBLE : View.GONE);
mHistoryEmptyView.setVisibility(showEmptyHistoryView ? View.VISIBLE : View.GONE);
mRecentTabsEmptyView.setVisibility(showEmptyRecentTabsView ? View.VISIBLE : View.GONE);
}
/**
@ -555,6 +649,13 @@ public class CombinedHistoryPanel extends HomeFragment implements RemoteClientsD
}
}
@Override
public void onDestroyView() {
super.onDestroyView();
mRecentTabsAdapter.stopListeningForClosedTabs();
}
@Override
public void onDestroy() {
super.onDestroy();

View File

@ -19,7 +19,8 @@ import org.mozilla.gecko.widget.RecyclerViewClickSupport;
import java.util.EnumSet;
import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.CHILD;
import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.CHILD_RECENT_TABS;
import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.CHILD_SYNC;
import static org.mozilla.gecko.home.CombinedHistoryPanel.OnPanelLevelChangeListener.PanelLevel.PARENT;
public class CombinedHistoryRecyclerView extends RecyclerView
@ -89,10 +90,15 @@ public class CombinedHistoryRecyclerView extends RecyclerView
public void onItemClicked(RecyclerView recyclerView, int position, View v) {
final int viewType = getAdapter().getItemViewType(position);
final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType);
final String telemetryExtra;
switch (itemType) {
case RECENT_TABS:
mOnPanelLevelChangeListener.changeLevel(CHILD_RECENT_TABS);
break;
case SYNCED_DEVICES:
mOnPanelLevelChangeListener.changeLevel(CHILD);
mOnPanelLevelChangeListener.changeLevel(CHILD_SYNC);
break;
case CLIENT:
@ -117,6 +123,11 @@ public class CombinedHistoryRecyclerView extends RecyclerView
mOnUrlOpenListener.onUrlOpen(historyItem.getUrl(), EnumSet.of(HomePager.OnUrlOpenListener.Flags.ALLOW_SWITCH_TO_TAB));
}
break;
case CLOSED_TAB:
telemetryExtra = ((RecentTabsAdapter) getAdapter()).restoreTabFromPosition(position);
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, telemetryExtra);
break;
}
}

View File

@ -43,13 +43,13 @@ public final class HomeConfig {
TOP_SITES("top_sites", TopSitesPanel.class),
BOOKMARKS("bookmarks", BookmarksPanel.class),
COMBINED_HISTORY("combined_history", CombinedHistoryPanel.class),
RECENT_TABS("recent_tabs", RecentTabsPanel.class),
DYNAMIC("dynamic", DynamicPanel.class),
// Deprecated panels that should no longer exist but are kept around for
// migration code. Class references have been replaced with new version of the panel.
DEPRECATED_REMOTE_TABS("remote_tabs", CombinedHistoryPanel.class),
DEPRECATED_HISTORY("history", CombinedHistoryPanel.class),
DEPRECATED_READING_LIST("reading_list", BookmarksPanel.class);
DEPRECATED_READING_LIST("reading_list", BookmarksPanel.class),
DEPRECATED_RECENT_TABS("recent_tabs", CombinedHistoryPanel.class);
private final String mId;
private final Class<?> mPanelClass;
@ -1644,12 +1644,10 @@ public final class HomeConfig {
case DEPRECATED_HISTORY:
case DEPRECATED_REMOTE_TABS:
case DEPRECATED_RECENT_TABS:
case COMBINED_HISTORY:
return R.string.home_history_title;
case RECENT_TABS:
return R.string.recent_tabs_title;
default:
throw new IllegalArgumentException("Only for built-in panel types: " + panelType);
}
@ -1675,7 +1673,7 @@ public final class HomeConfig {
case DEPRECATED_READING_LIST:
return DEPRECATED_READING_LIST_PANEL_ID;
case RECENT_TABS:
case DEPRECATED_RECENT_TABS:
return RECENT_TABS_PANEL_ID;
default:

View File

@ -35,7 +35,7 @@ public class HomeConfigPrefsBackend implements HomeConfigBackend {
private static final String LOGTAG = "GeckoHomeConfigBackend";
// Increment this to trigger a migration.
private static final int VERSION = 6;
private static final int VERSION = 7;
// This key was originally used to store only an array of panel configs.
public static final String PREFS_CONFIG_KEY_OLD = "home_panels";
@ -75,8 +75,6 @@ public class HomeConfigPrefsBackend implements HomeConfigBackend {
panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.COMBINED_HISTORY));
panelConfigs.add(createBuiltinPanelConfig(mContext, PanelType.RECENT_TABS));
return new State(panelConfigs, true);
}
@ -233,12 +231,23 @@ public class HomeConfigPrefsBackend implements HomeConfigBackend {
}
/**
* Remove the reading list panel.
* If the reading list panel used to be the default panel, we make bookmarks the new default.
* Removes a panel from the home panel config.
* If the removed panel was set as the default home panel, we provide a replacement for it.
*
* @param context Android context
* @param jsonPanels array of original JSON panels
* @param panelToRemove The home panel to be removed.
* @param replacementPanel The panel which will replace it if the removed panel
* was the default home panel.
* @param alwaysUnhide If true, the replacement panel will always be unhidden,
* otherwise only if we turn it into the new default panel.
* @return new array of updated JSON panels
* @throws JSONException
*/
private static JSONArray removeReadingListPanel(Context context, JSONArray jsonPanels) throws JSONException {
private static JSONArray removePanel(Context context, JSONArray jsonPanels,
PanelType panelToRemove, PanelType replacementPanel, boolean alwaysUnhide) throws JSONException {
boolean wasDefault = false;
int bookmarksIndex = -1;
int replacementPanelIndex = -1;
// JSONArrary doesn't provide remove() for API < 19, therefore we need to manually copy all
// the items we don't want deleted into a new array.
@ -248,26 +257,33 @@ public class HomeConfigPrefsBackend implements HomeConfigBackend {
final JSONObject panelJSON = jsonPanels.getJSONObject(i);
final PanelConfig panelConfig = new PanelConfig(panelJSON);
if (panelConfig.getType() == PanelType.DEPRECATED_READING_LIST) {
if (panelConfig.getType() == panelToRemove) {
// If this panel was the default we'll need to assign a new default:
wasDefault = panelConfig.isDefault();
} else {
if (panelConfig.getType() == PanelType.BOOKMARKS) {
bookmarksIndex = newJSONPanels.length();
if (panelConfig.getType() == replacementPanel) {
replacementPanelIndex = newJSONPanels.length();
}
newJSONPanels.put(panelJSON);
}
}
if (wasDefault) {
// This will make the bookmarks panel visible if it was previously hidden - this is desired
// since this will make the new equivalent of the reading list visible by default.
final JSONObject bookmarksPanelConfig = createBuiltinPanelConfig(context, PanelType.BOOKMARKS, EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)).toJSON();
if (bookmarksIndex != -1) {
newJSONPanels.put(bookmarksIndex, bookmarksPanelConfig);
// Unless alwaysUnhide is true, we make the replacement panel visible only if it is going
// to be the new default panel, since a hidden default panel doesn't make sense.
// This is to allow preserving the behaviour of the original reading list migration function.
if (wasDefault || alwaysUnhide) {
final JSONObject replacementPanelConfig;
if (wasDefault) {
replacementPanelConfig = createBuiltinPanelConfig(context, replacementPanel, EnumSet.of(PanelConfig.Flags.DEFAULT_PANEL)).toJSON();
} else {
newJSONPanels.put(bookmarksPanelConfig);
replacementPanelConfig = createBuiltinPanelConfig(context, replacementPanel).toJSON();
}
if (replacementPanelIndex != -1) {
newJSONPanels.put(replacementPanelIndex, replacementPanelConfig);
} else {
newJSONPanels.put(replacementPanelConfig);
}
}
@ -346,7 +362,7 @@ public class HomeConfigPrefsBackend implements HomeConfigBackend {
case 1:
// Add "Recent Tabs" panel.
addBuiltinPanelConfig(context, jsonPanels,
PanelType.RECENT_TABS, Position.FRONT, Position.BACK);
PanelType.DEPRECATED_RECENT_TABS, Position.FRONT, Position.BACK);
// Remove the old pref key.
prefsEditor.remove(PREFS_CONFIG_KEY_OLD);
@ -384,7 +400,13 @@ public class HomeConfigPrefsBackend implements HomeConfigBackend {
break;
case 6:
jsonPanels = removeReadingListPanel(context, jsonPanels);
jsonPanels = removePanel(context, jsonPanels,
PanelType.DEPRECATED_READING_LIST, PanelType.BOOKMARKS, false);
break;
case 7:
jsonPanels = removePanel(context, jsonPanels,
PanelType.DEPRECATED_RECENT_TABS, PanelType.COMBINED_HISTORY, true);
break;
}
}

View File

@ -0,0 +1,423 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.home;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.AboutPages;
import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.R;
import org.mozilla.gecko.SessionParser;
import org.mozilla.gecko.home.CombinedHistoryAdapter.RecentTabsUpdateHandler;
import org.mozilla.gecko.home.CombinedHistoryPanel.PanelStateUpdateHandler;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.NativeEventListener;
import org.mozilla.gecko.util.NativeJSObject;
import org.mozilla.gecko.util.ThreadUtils;
import java.util.ArrayList;
import java.util.List;
import static org.mozilla.gecko.home.CombinedHistoryItem.ItemType;
public class RecentTabsAdapter extends RecyclerView.Adapter<CombinedHistoryItem>
implements CombinedHistoryRecyclerView.AdapterContextMenuBuilder, NativeEventListener {
private static final String LOGTAG = "GeckoRecentTabsAdapter";
private static final int NAVIGATION_BACK_BUTTON_INDEX = 0;
private static final String TELEMETRY_EXTRA_LAST_TIME = "recent_tabs_last_time";
private static final String TELEMETRY_EXTRA_RECENTY_CLOSED = "recent_closed_tabs";
private static final String TELEMETRY_EXTRA_MIXED = "recent_tabs_mixed";
// Recently closed tabs from Gecko.
private ClosedTab[] recentlyClosedTabs;
// "Tabs from last time".
private ClosedTab[] lastSessionTabs;
public static final class ClosedTab {
public final String url;
public final String title;
public final String data;
public ClosedTab(String url, String title, String data) {
this.url = url;
this.title = title;
this.data = data;
}
}
private final Context context;
private final RecentTabsUpdateHandler recentTabsUpdateHandler;
private final PanelStateUpdateHandler panelStateUpdateHandler;
public RecentTabsAdapter(Context context,
RecentTabsUpdateHandler recentTabsUpdateHandler,
PanelStateUpdateHandler panelStateUpdateHandler) {
this.context = context;
this.recentTabsUpdateHandler = recentTabsUpdateHandler;
this.panelStateUpdateHandler = panelStateUpdateHandler;
recentlyClosedTabs = new ClosedTab[0];
lastSessionTabs = new ClosedTab[0];
readPreviousSessionData();
}
public void startListeningForClosedTabs() {
EventDispatcher.getInstance().registerGeckoThreadListener(this, "ClosedTabs:Data");
GeckoAppShell.notifyObservers("ClosedTabs:StartNotifications", null);
}
public void stopListeningForClosedTabs() {
GeckoAppShell.notifyObservers("ClosedTabs:StopNotifications", null);
EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "ClosedTabs:Data");
}
@Override
public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
final NativeJSObject[] tabs = message.getObjectArray("tabs");
final int length = tabs.length;
final ClosedTab[] closedTabs = new ClosedTab[length];
for (int i = 0; i < length; i++) {
final NativeJSObject tab = tabs[i];
closedTabs[i] = new ClosedTab(tab.getString("url"), tab.getString("title"), tab.getObject("data").toString());
}
// Only modify recentlyClosedTabs on the UI thread.
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
// Save some data about the old panel state, so we can be
// smarter about notifying the recycler view which bits changed.
int prevClosedTabsCount = recentlyClosedTabs.length;
boolean prevSectionHeaderVisibility = isSectionHeaderVisible();
int prevSectionHeaderIndex = getSectionHeaderIndex();
recentlyClosedTabs = closedTabs;
recentTabsUpdateHandler.onRecentTabsCountUpdated(getClosedTabsCount());
panelStateUpdateHandler.onPanelStateUpdated();
// Handle the section header hiding/unhiding.
updateHeaderVisibility(prevSectionHeaderVisibility, prevSectionHeaderIndex);
// Update the "Recently closed" part of the tab list.
updateTabsList(prevClosedTabsCount, recentlyClosedTabs.length, getFirstRecentTabIndex(), getLastRecentTabIndex());
}
});
}
private void readPreviousSessionData() {
// Make sure that the start up code has had a chance to update sessionstore.bak as necessary.
GeckoProfile.get(context).waitForOldSessionDataProcessing();
ThreadUtils.postToBackgroundThread(new Runnable() {
@Override
public void run() {
final String jsonString = GeckoProfile.get(context).readSessionFile(true);
if (jsonString == null) {
// No previous session data.
return;
}
final List<ClosedTab> parsedTabs = new ArrayList<>();
new SessionParser() {
@Override
public void onTabRead(SessionTab tab) {
final String url = tab.getUrl();
// Don't show last tabs for about:home
if (AboutPages.isAboutHome(url)) {
return;
}
parsedTabs.add(new ClosedTab(url, tab.getTitle(), tab.getTabObject().toString()));
}
}.parse(jsonString);
final ClosedTab[] closedTabs = parsedTabs.toArray(new ClosedTab[parsedTabs.size()]);
// Only modify lastSessionTabs on the UI thread.
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
// Save some data about the old panel state, so we can be
// smarter about notifying the recycler view which bits changed.
int prevClosedTabsCount = lastSessionTabs.length;
boolean prevSectionHeaderVisibility = isSectionHeaderVisible();
int prevSectionHeaderIndex = getSectionHeaderIndex();
lastSessionTabs = closedTabs;
recentTabsUpdateHandler.onRecentTabsCountUpdated(getClosedTabsCount());
panelStateUpdateHandler.onPanelStateUpdated();
// Handle the section header hiding/unhiding.
updateHeaderVisibility(prevSectionHeaderVisibility, prevSectionHeaderIndex);
// Update the "Tabs from last time" part of the tab list.
updateTabsList(prevClosedTabsCount, lastSessionTabs.length, getFirstLastSessionTabIndex(), getLastLastSessionTabIndex());
}
});
}
});
}
public void clearLastSessionData() {
final ClosedTab[] emptyLastSessionTabs = new ClosedTab[0];
// Only modify mLastSessionTabs on the UI thread.
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
// Save some data about the old panel state, so we can be
// smarter about notifying the recycler view which bits changed.
int prevClosedTabsCount = lastSessionTabs.length;
boolean prevSectionHeaderVisibility = isSectionHeaderVisible();
int prevSectionHeaderIndex = getSectionHeaderIndex();
lastSessionTabs = emptyLastSessionTabs;
recentTabsUpdateHandler.onRecentTabsCountUpdated(getClosedTabsCount());
panelStateUpdateHandler.onPanelStateUpdated();
// Handle the section header hiding.
updateHeaderVisibility(prevSectionHeaderVisibility, prevSectionHeaderIndex);
// Handle the "tabs from last time" being cleared.
if (prevClosedTabsCount > 0) {
notifyItemRangeRemoved(getFirstLastSessionTabIndex(), prevClosedTabsCount);
}
}
});
}
private void updateHeaderVisibility(boolean prevSectionHeaderVisibility, int prevSectionHeaderIndex) {
if (prevSectionHeaderVisibility && !isSectionHeaderVisible()) {
notifyItemRemoved(prevSectionHeaderIndex);
} else if (!prevSectionHeaderVisibility && isSectionHeaderVisible()) {
notifyItemInserted(getSectionHeaderIndex());
}
}
/**
* Updates the tab list as necessary to account for any changes in tab count in a particular data source.
*
* Since the session store only sends out full updates, we don't know for sure what has changed compared
* to the last data set, so we can only animate if the tab count actually changes.
*
* @param prevClosedTabsCount The previous number of closed tabs from that data source.
* @param closedTabsCount The current number of closed tabs contained in that data source.
* @param firstTabListIndex The current position of that data source's first item in the RecyclerView.
* @param lastTabListIndex The current position of that data source's last item in the RecyclerView.
*/
private void updateTabsList(int prevClosedTabsCount, int closedTabsCount, int firstTabListIndex, int lastTabListIndex) {
final int closedTabsCountChange = closedTabsCount - prevClosedTabsCount;
if (closedTabsCountChange <= 0) {
notifyItemRangeRemoved(lastTabListIndex + 1, -closedTabsCountChange); // Remove tabs from the bottom of the list.
notifyItemRangeChanged(firstTabListIndex, closedTabsCount); // Update the contents of the remaining items.
} else { // closedTabsCountChange > 0
notifyItemRangeInserted(firstTabListIndex, closedTabsCountChange); // Add additional tabs at the top of the list.
notifyItemRangeChanged(firstTabListIndex + closedTabsCountChange, prevClosedTabsCount); // Update any previous list items.
}
}
public String restoreTabFromPosition(int position) {
final List<String> dataList = new ArrayList<>(1);
dataList.add(getClosedTabForPosition(position).data);
final String telemetryExtra =
position > getLastRecentTabIndex() ? TELEMETRY_EXTRA_LAST_TIME : TELEMETRY_EXTRA_RECENTY_CLOSED;
restoreSessionWithHistory(dataList);
return telemetryExtra;
}
public String restoreAllTabs() {
if (recentlyClosedTabs.length == 0 && lastSessionTabs.length == 0) {
return null;
}
final List<String> dataList = new ArrayList<>(getClosedTabsCount());
addTabDataToList(dataList, recentlyClosedTabs);
addTabDataToList(dataList, lastSessionTabs);
final String telemetryExtra = recentlyClosedTabs.length > 0 && lastSessionTabs.length > 0 ? TELEMETRY_EXTRA_MIXED :
recentlyClosedTabs.length > 0 ? TELEMETRY_EXTRA_RECENTY_CLOSED : TELEMETRY_EXTRA_LAST_TIME;
restoreSessionWithHistory(dataList);
return telemetryExtra;
}
private void addTabDataToList(List<String> dataList, ClosedTab[] closedTabs) {
for (ClosedTab closedTab : closedTabs) {
dataList.add(closedTab.data);
}
}
private static void restoreSessionWithHistory(List<String> dataList) {
final JSONObject json = new JSONObject();
try {
json.put("tabs", new JSONArray(dataList));
} catch (JSONException e) {
Log.e(LOGTAG, "JSON error", e);
}
GeckoAppShell.notifyObservers("Session:RestoreRecentTabs", json.toString());
}
@Override
public CombinedHistoryItem onCreateViewHolder(ViewGroup parent, int viewType) {
final LayoutInflater inflater = LayoutInflater.from(parent.getContext());
final View view;
final CombinedHistoryItem.ItemType itemType = CombinedHistoryItem.ItemType.viewTypeToItemType(viewType);
switch (itemType) {
case NAVIGATION_BACK:
view = inflater.inflate(R.layout.home_combined_back_item, parent, false);
return new CombinedHistoryItem.HistoryItem(view);
case SECTION_HEADER:
view = inflater.inflate(R.layout.home_header_row, parent, false);
return new CombinedHistoryItem.BasicItem(view);
case CLOSED_TAB:
view = inflater.inflate(R.layout.home_item_row, parent, false);
return new CombinedHistoryItem.HistoryItem(view);
}
return null;
}
@Override
public void onBindViewHolder(CombinedHistoryItem holder, final int position) {
final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
switch (itemType) {
case SECTION_HEADER:
((TextView) holder.itemView).setText(context.getString(R.string.home_closed_tabs_title2));
break;
case CLOSED_TAB:
final ClosedTab closedTab = getClosedTabForPosition(position);
((CombinedHistoryItem.HistoryItem) holder).bind(closedTab);
break;
}
}
@Override
public int getItemCount() {
int itemCount = 1; // NAVIGATION_BACK button is always visible.
if (isSectionHeaderVisible()) {
itemCount += 1;
}
itemCount += getClosedTabsCount();
return itemCount;
}
private CombinedHistoryItem.ItemType getItemTypeForPosition(int position) {
if (position == NAVIGATION_BACK_BUTTON_INDEX) {
return ItemType.NAVIGATION_BACK;
}
if (position == getSectionHeaderIndex() && isSectionHeaderVisible()) {
return ItemType.SECTION_HEADER;
}
return ItemType.CLOSED_TAB;
}
@Override
public int getItemViewType(int position) {
return CombinedHistoryItem.ItemType.itemTypeToViewType(getItemTypeForPosition(position));
}
public int getClosedTabsCount() {
return recentlyClosedTabs.length + lastSessionTabs.length;
}
private boolean isSectionHeaderVisible() {
return recentlyClosedTabs.length > 0 || lastSessionTabs.length > 0;
}
private int getSectionHeaderIndex() {
return isSectionHeaderVisible() ?
NAVIGATION_BACK_BUTTON_INDEX + 1 :
NAVIGATION_BACK_BUTTON_INDEX;
}
private int getFirstRecentTabIndex() {
return getSectionHeaderIndex() + 1;
}
private int getLastRecentTabIndex() {
return getSectionHeaderIndex() + recentlyClosedTabs.length;
}
private int getFirstLastSessionTabIndex() {
return getLastRecentTabIndex() + 1;
}
private int getLastLastSessionTabIndex() {
return getLastRecentTabIndex() + lastSessionTabs.length;
}
/**
* Get the closed tab corresponding to a RecyclerView list item.
*
* The Recent Tab folder combines two data sources, so if we want to get the ClosedTab object
* behind a certain list item, we need to route this request to the corresponding data source
* and also transform the global list position into a local array index.
*/
private ClosedTab getClosedTabForPosition(int position) {
final ClosedTab closedTab;
if (position <= getLastRecentTabIndex()) { // Upper part of the list is "Recently closed tabs".
closedTab = recentlyClosedTabs[position - getFirstRecentTabIndex()];
} else { // Lower part is "Tabs from last time".
closedTab = lastSessionTabs[position - getFirstLastSessionTabIndex()];
}
return closedTab;
}
@Override
public HomeContextMenuInfo makeContextMenuInfoFromPosition(View view, int position) {
final CombinedHistoryItem.ItemType itemType = getItemTypeForPosition(position);
final HomeContextMenuInfo info;
switch (itemType) {
case CLOSED_TAB:
info = new HomeContextMenuInfo(view, position, -1);
ClosedTab closedTab = getClosedTabForPosition(position);
return populateChildInfoFromTab(info, closedTab);
}
return null;
}
protected static HomeContextMenuInfo populateChildInfoFromTab(HomeContextMenuInfo info, ClosedTab tab) {
info.url = tab.url;
info.title = tab.title;
return info;
}
}

View File

@ -1,443 +0,0 @@
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.home;
import java.util.ArrayList;
import java.util.List;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.AboutPages;
import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.GeckoAppShell;
import org.mozilla.gecko.GeckoEvent;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.R;
import org.mozilla.gecko.SessionParser;
import org.mozilla.gecko.Telemetry;
import org.mozilla.gecko.TelemetryContract;
import org.mozilla.gecko.db.BrowserContract.CommonColumns;
import org.mozilla.gecko.db.BrowserContract.URLColumns;
import org.mozilla.gecko.reader.SavedReaderViewHelper;
import org.mozilla.gecko.util.EventCallback;
import org.mozilla.gecko.util.NativeEventListener;
import org.mozilla.gecko.util.NativeJSObject;
import org.mozilla.gecko.util.ThreadUtils;
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.database.MatrixCursor.RowBuilder;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.Loader;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewStub;
import android.widget.AdapterView;
import android.widget.ImageView;
import android.widget.TextView;
/**
* Fragment that displays tabs from last session in a ListView.
*/
public class RecentTabsPanel extends HomeFragment
implements NativeEventListener {
// Logging tag name
@SuppressWarnings("unused")
private static final String LOGTAG = "GeckoRecentTabsPanel";
// Cursor loader ID for the loader that loads recent tabs
private static final int LOADER_ID_RECENT_TABS = 0;
private static final String TELEMETRY_EXTRA_LAST_TIME = "recent_tabs_last_time";
private static final String TELEMETRY_EXTRA_CLOSED = "recent_closed_tabs";
// Adapter for the list of recent tabs.
private RecentTabsAdapter mAdapter;
// The view shown by the fragment.
private HomeListView mList;
// Reference to the View to display when there are no results.
private View mEmptyView;
// Callbacks used for the search and favicon cursor loaders
private CursorLoaderCallbacks mCursorLoaderCallbacks;
// Recently closed tabs from gecko
private ClosedTab[] mClosedTabs;
private void restoreSessionWithHistory(List<String> dataList) {
JSONObject json = new JSONObject();
try {
json.put("tabs", new JSONArray(dataList));
} catch (JSONException e) {
Log.e(LOGTAG, "JSON error", e);
}
GeckoAppShell.notifyObservers("Session:RestoreRecentTabs", json.toString());
}
private static final class ClosedTab {
public final String url;
public final String title;
public final String data;
public ClosedTab(String url, String title, String data) {
this.url = url;
this.title = title;
this.data = data;
}
}
public static final class RecentTabs implements URLColumns, CommonColumns {
public static final String TYPE = "type";
public static final String DATA = "data";
public static final int TYPE_HEADER = 0;
public static final int TYPE_LAST_TIME = 1;
public static final int TYPE_CLOSED = 2;
public static final int TYPE_OPEN_ALL_LAST_TIME = 3;
public static final int TYPE_OPEN_ALL_CLOSED = 4;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
return inflater.inflate(R.layout.home_list_panel, container, false);
}
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
mList = (HomeListView) view.findViewById(R.id.list);
mList.setTag(HomePager.LIST_TAG_RECENT_TABS);
mList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
final Cursor c = mAdapter.getCursor();
if (c == null || !c.moveToPosition(position)) {
return;
}
final int itemType = c.getInt(c.getColumnIndexOrThrow(RecentTabs.TYPE));
if (itemType == RecentTabs.TYPE_OPEN_ALL_LAST_TIME) {
openTabsWithType(RecentTabs.TYPE_LAST_TIME);
return;
}
if (itemType == RecentTabs.TYPE_OPEN_ALL_CLOSED) {
openTabsWithType(RecentTabs.TYPE_CLOSED);
return;
}
final String extras = (itemType == RecentTabs.TYPE_CLOSED) ? TELEMETRY_EXTRA_CLOSED : TELEMETRY_EXTRA_LAST_TIME;
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.LIST_ITEM, extras);
final List<String> dataList = new ArrayList<>();
dataList.add(c.getString(c.getColumnIndexOrThrow(RecentTabs.DATA)));
restoreSessionWithHistory(dataList);
}
});
mList.setContextMenuInfoFactory(new HomeContextMenuInfo.Factory() {
@Override
public HomeContextMenuInfo makeInfoForCursor(View view, int position, long id, Cursor cursor) {
// Don't show context menus for the "Open all" rows.
final int itemType = cursor.getInt(cursor.getColumnIndexOrThrow(RecentTabs.TYPE));
if (itemType == RecentTabs.TYPE_OPEN_ALL_LAST_TIME || itemType == RecentTabs.TYPE_OPEN_ALL_CLOSED) {
return null;
}
final HomeContextMenuInfo info = new HomeContextMenuInfo(view, position, id);
info.url = cursor.getString(cursor.getColumnIndexOrThrow(RecentTabs.URL));
info.title = cursor.getString(cursor.getColumnIndexOrThrow(RecentTabs.TITLE));
return info;
}
});
registerForContextMenu(mList);
EventDispatcher.getInstance().registerGeckoThreadListener(this, "ClosedTabs:Data");
GeckoAppShell.notifyObservers("ClosedTabs:StartNotifications", null);
}
@Override
public void onDestroyView() {
super.onDestroyView();
// Discard any additional item clicks on the list as the
// panel is getting destroyed (bug 1210243).
mList.setOnItemClickListener(null);
mList = null;
mEmptyView = null;
EventDispatcher.getInstance().unregisterGeckoThreadListener(this, "ClosedTabs:Data");
GeckoAppShell.notifyObservers("ClosedTabs:StopNotifications", null);
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Intialize adapter
mAdapter = new RecentTabsAdapter(getActivity());
mList.setAdapter(mAdapter);
// Create callbacks before the initial loader is started
mCursorLoaderCallbacks = new CursorLoaderCallbacks();
loadIfVisible();
}
private void updateUiFromCursor(Cursor c) {
if (c != null && c.getCount() > 0) {
return;
}
if (mEmptyView == null) {
// Set empty panel view. We delay this so that the empty view won't flash.
final ViewStub emptyViewStub = (ViewStub) getView().findViewById(R.id.home_empty_view_stub);
mEmptyView = emptyViewStub.inflate();
final ImageView emptyIcon = (ImageView) mEmptyView.findViewById(R.id.home_empty_image);
emptyIcon.setImageResource(R.drawable.icon_remote_tabs_empty);
final TextView emptyText = (TextView) mEmptyView.findViewById(R.id.home_empty_text);
emptyText.setText(R.string.home_last_tabs_empty);
mList.setEmptyView(mEmptyView);
}
}
@Override
protected void load() {
getLoaderManager().initLoader(LOADER_ID_RECENT_TABS, null, mCursorLoaderCallbacks);
}
@Override
public void handleMessage(String event, NativeJSObject message, EventCallback callback) {
final NativeJSObject[] tabs = message.getObjectArray("tabs");
final int length = tabs.length;
final ClosedTab[] closedTabs = new ClosedTab[length];
for (int i = 0; i < length; i++) {
final NativeJSObject tab = tabs[i];
closedTabs[i] = new ClosedTab(tab.getString("url"), tab.getString("title"), tab.getObject("data").toString());
}
// Only modify mClosedTabs on the UI thread
ThreadUtils.postToUiThread(new Runnable() {
@Override
public void run() {
mClosedTabs = closedTabs;
// The fragment might have been detached before this code
// runs in the UI thread.
if (getActivity() != null) {
// Reload the cursor to show recently closed tabs.
getLoaderManager().restartLoader(LOADER_ID_RECENT_TABS, null, mCursorLoaderCallbacks);
}
}
});
}
private void openTabsWithType(int type) {
final Cursor c = mAdapter.getCursor();
if (c == null || !c.moveToFirst()) {
return;
}
final List<String> dataList = new ArrayList<String>();
do {
if (c.getInt(c.getColumnIndexOrThrow(RecentTabs.TYPE)) == type) {
dataList.add(c.getString(c.getColumnIndexOrThrow(RecentTabs.DATA)));
}
} while (c.moveToNext());
final String extras = (type == RecentTabs.TYPE_CLOSED) ? TELEMETRY_EXTRA_CLOSED : TELEMETRY_EXTRA_LAST_TIME;
Telemetry.sendUIEvent(TelemetryContract.Event.LOAD_URL, TelemetryContract.Method.BUTTON, extras);
restoreSessionWithHistory(dataList);
}
private static class RecentTabsCursorLoader extends SimpleCursorLoader {
private final ClosedTab[] closedTabs;
public RecentTabsCursorLoader(Context context, ClosedTab[] closedTabs) {
super(context);
this.closedTabs = closedTabs;
}
private void addRow(MatrixCursor c, String url, String title, int type, String data) {
final RowBuilder row = c.newRow();
row.add(-1);
row.add(url);
row.add(title);
row.add(type);
row.add(data);
}
@Override
public Cursor loadCursor() {
// TwoLinePageRow requires the SavedReaderViewHelper to be initialised. Usually this is
// done as part of BrowserDatabaseHelper.onOpen(), however we don't actually access
// the DB when showing the Recent Tabs panel, hence it's possible that the SavedReaderViewHelper
// isn't loaded. Therefore we need to explicitly force loading here.
// Note: loadCursor is run on a background thread, hence it's safe to do this here.
// (loading time is a few ms, and hence shouldn't impact overall loading time for this
// panel in any significant way).
SavedReaderViewHelper.getSavedReaderViewHelper(getContext()).loadItems();
final Context context = getContext();
final MatrixCursor c = new MatrixCursor(new String[] { RecentTabs._ID,
RecentTabs.URL,
RecentTabs.TITLE,
RecentTabs.TYPE,
RecentTabs.DATA});
if (closedTabs != null && closedTabs.length > 0) {
// How many closed tabs are actually displayed.
int visibleClosedTabs = 0;
final int length = closedTabs.length;
for (int i = 0; i < length; i++) {
final String url = closedTabs[i].url;
// Don't show recent tabs for about:home or about:privatebrowsing.
if (!AboutPages.isTitlelessAboutPage(url)) {
// If this is the first closed tab we're adding, add a header for the section.
if (visibleClosedTabs == 0) {
addRow(c, null, context.getString(R.string.home_closed_tabs_title), RecentTabs.TYPE_HEADER, null);
}
addRow(c, url, closedTabs[i].title, RecentTabs.TYPE_CLOSED, closedTabs[i].data);
visibleClosedTabs++;
}
}
// Add an "Open all" button if more than 2 tabs were added to the list.
if (visibleClosedTabs > 1) {
addRow(c, null, null, RecentTabs.TYPE_OPEN_ALL_CLOSED, null);
}
}
// We need to ensure that the session restore code has updated sessionstore.bak as necessary.
GeckoProfile.get(context).waitForOldSessionDataProcessing();
final String jsonString = GeckoProfile.get(context).readSessionFile(true);
if (jsonString == null) {
// No previous session data
return c;
}
final int count = c.getCount();
new SessionParser() {
@Override
public void onTabRead(SessionTab tab) {
final String url = tab.getUrl();
// Don't show last tabs for about:home
if (AboutPages.isAboutHome(url)) {
return;
}
// If this is the first tab we're reading, add a header.
if (c.getCount() == count) {
addRow(c, null, context.getString(R.string.home_last_tabs_title), RecentTabs.TYPE_HEADER, null);
}
addRow(c, url, tab.getTitle(), RecentTabs.TYPE_LAST_TIME, tab.getTabObject().toString());
}
}.parse(jsonString);
// Add an "Open all" button if more than 2 tabs were added to the list (account for the header)
if (c.getCount() - count > 2) {
addRow(c, null, null, RecentTabs.TYPE_OPEN_ALL_LAST_TIME, null);
}
return c;
}
}
private static class RecentTabsAdapter extends MultiTypeCursorAdapter {
private static final int ROW_HEADER = 0;
private static final int ROW_STANDARD = 1;
private static final int ROW_OPEN_ALL = 2;
private static final int[] VIEW_TYPES = new int[] { ROW_STANDARD, ROW_HEADER, ROW_OPEN_ALL };
private static final int[] LAYOUT_TYPES =
new int[] { R.layout.home_item_row, R.layout.home_header_row, R.layout.home_open_all_row };
public RecentTabsAdapter(Context context) {
super(context, null, VIEW_TYPES, LAYOUT_TYPES);
}
@Override
public int getItemViewType(int position) {
final Cursor c = getCursor(position);
final int type = c.getInt(c.getColumnIndexOrThrow(RecentTabs.TYPE));
if (type == RecentTabs.TYPE_HEADER) {
return ROW_HEADER;
}
if (type == RecentTabs.TYPE_OPEN_ALL_LAST_TIME || type == RecentTabs.TYPE_OPEN_ALL_CLOSED) {
return ROW_OPEN_ALL;
}
return ROW_STANDARD;
}
@Override
public boolean isEnabled(int position) {
return (getItemViewType(position) != ROW_HEADER);
}
@Override
public void bindView(View view, Context context, int position) {
final int itemType = getItemViewType(position);
if (itemType == ROW_OPEN_ALL) {
return;
}
final Cursor c = getCursor(position);
if (itemType == ROW_HEADER) {
final String title = c.getString(c.getColumnIndexOrThrow(RecentTabs.TITLE));
final TextView textView = (TextView) view;
textView.setText(title);
} else if (itemType == ROW_STANDARD) {
final TwoLinePageRow pageRow = (TwoLinePageRow) view;
pageRow.setShowIcons(false);
pageRow.updateFromCursor(c);
}
}
}
private class CursorLoaderCallbacks implements LoaderManager.LoaderCallbacks<Cursor> {
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new RecentTabsCursorLoader(getActivity(), mClosedTabs);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
mAdapter.swapCursor(c);
updateUiFromCursor(c);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
mAdapter.swapCursor(null);
}
}
}

View File

@ -17,6 +17,8 @@ import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
import org.mozilla.gecko.GeckoProfile;
import org.mozilla.gecko.GeckoSharedPrefs;
import org.mozilla.gecko.preferences.GeckoPreferences;
import org.mozilla.gecko.restrictions.Restrictable;
import org.mozilla.gecko.restrictions.Restrictions;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.net.BaseResource;
import org.mozilla.gecko.sync.net.BaseResourceDelegate;
@ -202,6 +204,12 @@ public class TelemetryUploadService extends IntentService {
return false;
}
if (Restrictions.isRestrictedProfile(context) &&
!Restrictions.isAllowed(context, Restrictable.HEALTH_REPORT)) {
Log.d(LOGTAG, "Telemetry upload feature disabled by admin profile");
return false;
}
return true;
}

View File

@ -44,7 +44,6 @@
They are automatically converted to all caps by the Android platform. -->
<!ENTITY bookmarks_title "Bookmarks">
<!ENTITY history_title "History">
<!ENTITY recent_tabs_title "Recent Tabs">
<!ENTITY switch_to_tab "Switch to tab">
@ -570,10 +569,12 @@ size. -->
<!ENTITY home_clear_history_button "Clear browsing history">
<!ENTITY home_clear_history_confirm "Are you sure you want to clear your history?">
<!ENTITY home_bookmarks_empty "Bookmarks you save show up here.">
<!ENTITY home_closed_tabs_title "Recently closed tabs">
<!ENTITY home_last_tabs_title "Tabs from last time">
<!ENTITY home_closed_tabs_title2 "Recently closed">
<!ENTITY home_last_tabs_empty "Your recent tabs show up here.">
<!ENTITY home_open_all "Open all">
<!ENTITY home_restore_all "Restore all">
<!ENTITY home_closed_tabs_number "&formatD; tabs">
<!-- Localization note (home_closed_tabs_one): This is the singular version of home_closed_tabs_number, referring to the number of recently closed tabs available. -->
<!ENTITY home_closed_tabs_one "1 tab">
<!ENTITY home_most_recent_empty "Websites you visited most recently show up here.">
<!-- Localization note (home_most_recent_emptyhint2): "Psst" is a sound that might be used to attract someone's attention unobtrusively, and intended to hint at Private Browsing to the user.
The placeholders &formatS1; and &formatS2; are used to mark the location of text underlining. -->

View File

@ -431,7 +431,7 @@ gbjar.sources += ['java/org/mozilla/gecko/' + x for x in [
'home/PanelViewAdapter.java',
'home/PanelViewItemHandler.java',
'home/PinSiteDialog.java',
'home/RecentTabsPanel.java',
'home/RecentTabsAdapter.java',
'home/RemoteTabsExpandableListState.java',
'home/SearchEngine.java',
'home/SearchEngineAdapter.java',

Binary file not shown.

After

Width:  |  Height:  |  Size: 774 B

View File

@ -1,20 +0,0 @@
<?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/. -->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<ViewStub android:id="@+id/home_empty_view_stub"
android:layout="@layout/home_empty_panel"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<org.mozilla.gecko.home.HomeListView
android:id="@+id/list"
style="@style/Widget.Home.HomeList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</merge>

View File

@ -1,20 +0,0 @@
<?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/. -->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<ViewStub android:id="@+id/home_empty_view_stub"
android:layout="@layout/home_empty_panel"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<org.mozilla.gecko.home.HomeListView
android:id="@+id/list"
style="@style/Widget.Home.HomeList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</merge>

View File

@ -12,7 +12,7 @@
android:id="@+id/refresh_layout"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
android:layout_weight="1.2">
<org.mozilla.gecko.home.CombinedHistoryRecyclerView
android:id="@+id/combined_recycler_view"
@ -36,9 +36,15 @@
android:layout_weight="3"
android:visibility="gone"/>
<Button android:id="@+id/clear_history_button"
<include android:id="@+id/home_recent_tabs_empty_view"
layout="@layout/home_empty_panel"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="3"
android:visibility="gone"/>
<Button android:id="@+id/history_panel_footer_button"
style="@style/Widget.Home.ActionButton"
android:text="@string/home_clear_history_button"
android:layout_width="match_parent"
android:layout_height="48dp"
android:visibility="gone" />

View File

@ -1,20 +0,0 @@
<?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/. -->
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<ViewStub android:id="@+id/home_empty_view_stub"
android:layout="@layout/home_empty_panel"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<org.mozilla.gecko.home.HomeListView
android:id="@+id/list"
style="@style/Widget.Home.HomeList"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"/>
</merge>

View File

@ -1,13 +0,0 @@
<?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/. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/home_list"/>
</LinearLayout>

View File

@ -1,8 +0,0 @@
<?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/. -->
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:text="@string/home_open_all"
style="@style/Widget.Home.ActionItem"/>

View File

@ -65,7 +65,6 @@
<string name="bookmarks_title">&bookmarks_title;</string>
<string name="history_title">&history_title;</string>
<string name="recent_tabs_title">&recent_tabs_title;</string>
<string name="switch_to_tab">&switch_to_tab;</string>
@ -452,10 +451,11 @@
<string name="home_clear_history_button">&home_clear_history_button;</string>
<string name="home_clear_history_confirm">&home_clear_history_confirm;</string>
<string name="home_bookmarks_empty">&home_bookmarks_empty;</string>
<string name="home_closed_tabs_title">&home_closed_tabs_title;</string>
<string name="home_last_tabs_title">&home_last_tabs_title;</string>
<string name="home_closed_tabs_title2">&home_closed_tabs_title2;</string>
<string name="home_last_tabs_empty">&home_last_tabs_empty;</string>
<string name="home_open_all">&home_open_all;</string>
<string name="home_restore_all">&home_restore_all;</string>
<string name="home_closed_tabs_number">&home_closed_tabs_number;</string>
<string name="home_closed_tabs_one">&home_closed_tabs_one;</string>
<string name="home_most_recent_empty">&home_most_recent_empty;</string>
<string name="home_most_recent_emptyhint">&home_most_recent_emptyhint2;</string>
<string name="home_default_empty">&home_default_empty;</string>

View File

@ -5,6 +5,9 @@
XPCOMUtils.defineLazyModuleGetter(this, "EventEmitter",
"resource://devtools/shared/event-emitter.js");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
// Import the android PageActions module.
XPCOMUtils.defineLazyModuleGetter(this, "PageActions",
"resource://gre/modules/PageActions.jsm");
@ -23,12 +26,22 @@ function PageAction(options, extension) {
let DEFAULT_ICON = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACQAAAAkCAYAAADhAJiYAAAC4klEQVRYhdWXLWzbQBSADQtDAwsHC1tUhUxqfL67lk2tdn+OJg0ODU0rLByqgqINBY6tmlbn7LMTJ5FaFVVBk1G0oUGjG2jT2Y7jxmmcbU/6iJ+f36fz+e5sGP9riCGm9hB37RG+scd4Yo/wsDXCZyIE2xuXsce4bY+wXkAsQtzYmExrfFgvkJkRbkzo1ehoxx5iXcgI/9iYUGt8WH9MqDXEcmNChmEYrRCf2SHWeYgQx3x0tLNRIeKQLTtEFyJEep4NTuhk8BC+yMrwEE3+iozo42d8gK7FAOkMsRiiN8QhW2ttSK5QTfRRV4QoymVeJMvPvDp7gCZigD613MN6yRFA3SWarow9QB9LCfG+NeF9qCtjAKOSQjCqVKhfVsiHEQ+grgx/lRGqUihAc1uL8EFD+KCRO+GrF4J61phcoRoPoEzkYhZYpykh5sMb7kOdIeY+jHKur4QI4Feh4AFX1nVeLxrAvQchGsBz5ls6wa2QdwcvIcE2863bTH79KOvsz/uUYJsp+J0pSzNlDckVqqVGUAF+n6uS7txcOl6wot4JVy70ufDLy4pWLUQVPE81pRI0mGe9oxLMHSeohHvMs/STUNaUK6vDPCvOyxMFDx4achehRDJmHnydnkPww5OFfLxrGIZBFDyYl4LpMzlTQFIP6AQx86w2UeYBccFpJrcKv5L9eGDtUAU6RIELqsB74uynjy/UBRF1gS5BTFxwQT1wTiXoUg9MH7m/3NZRRoi5IJytUbMgzv4Wc832+oQkiKgEehmyMkkpKsFkQV11QsRJL5rJYBLItQgRaUZEmnoZXsomz3vGiWw+I9KMF9SVFOqZEemZekli1jN3U/UOqhHHvC6oWWGElhfSpGdOk6+O9prdwvtLj5BjRsQxdRnot+Zeifpy/2/0stktKTRNLmbk0mwXyl8253fyojj+8rxOHNAhjjm5n0/5OOCGOKBzkrMO0Z75lvSAzKlrF32Z/3z8BqLAn+yMV7VhAAAAAElFTkSuQmCC";
this.popupUrl = options.default_popup;
this.options = {
title: options.default_title || extension.name,
icon: DEFAULT_ICON,
id: extension.id,
clickCallback: () => {
this.emit("click");
if (this.popupUrl) {
let win = Services.wm.getMostRecentWindow("navigator:browser");
win.BrowserApp.addTab(this.popupUrl, {
selected: true,
parentId: win.BrowserApp.selectedTab.id,
});
} else {
this.emit("click");
}
},
};
@ -50,6 +63,16 @@ PageAction.prototype = {
}
},
setPopup(tab, url) {
// TODO: Only set the popup for the specified tab once we have Tabs API support.
this.popupUrl = url;
},
getPopup(tab) {
// TODO: Only return the popup for the specified tab once we have Tabs API support.
return this.popupUrl;
},
shutdown() {
this.hide();
},
@ -85,9 +108,24 @@ extensions.registerSchemaAPI("pageAction", null, (extension, context) => {
show(tabId) {
pageActionMap.get(extension).show(tabId);
},
hide(tabId) {
pageActionMap.get(extension).hide(tabId);
},
setPopup(details) {
// TODO: Use the Tabs API to get the tab from details.tabId.
let tab = null;
let url = details.popup && context.uri.resolve(details.popup);
pageActionMap.get(extension).setPopup(tab, url);
},
getPopup(details) {
// TODO: Use the Tabs API to get the tab from details.tabId.
let tab = null;
let popup = pageActionMap.get(extension).getPopup(tab);
return Promise.resolve(popup);
},
},
};
});

View File

@ -24,7 +24,6 @@
"optional": true
},
"default_popup": {
"unsupported": true,
"type": "string",
"format": "relativeUrl",
"optional": true,
@ -161,7 +160,6 @@
},
{
"name": "setPopup",
"unsupported": true,
"type": "function",
"description": "Sets the html document to be opened as a popup when the user clicks on the page action's icon.",
"parameters": [
@ -180,7 +178,6 @@
},
{
"name": "getPopup",
"unsupported": true,
"type": "function",
"description": "Gets the html document set as the popup for this page action.",
"async": "callback",

View File

@ -2,4 +2,5 @@
support-files =
head.js
[test_ext_pageAction.html]
[test_ext_pageAction.html]
[test_ext_pageAction_popup.html]

View File

@ -0,0 +1,153 @@
<!DOCTYPE HTML>
<html>
<head>
<title>PageAction Test</title>
<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/SpawnTask.js"></script>
<script src="chrome://mochikit/content/tests/SimpleTest/ExtensionTestUtils.js"></script>
<script type="text/javascript" src="head.js"></script>
<link rel="stylesheet" href="chrome://mochikit/contents/tests/SimpleTest/test.css"/>
</head>
<body>
<script type="text/javascript">
"use strict";
Cu.import("resource://gre/modules/Services.jsm");
add_task(function* test_contentscript() {
function backgroundScript() {
// TODO: Use the Tabs API to obtain the tab ids for showing pageActions.
let tabId = 1;
let onClickedListenerEnabled = false;
browser.test.onMessage.addListener((msg, details) => {
if (msg === "page-action-show") {
// TODO: switch to using .show(tabId).then(...) once bug 1270742 lands.
browser.pageAction.show(tabId);
browser.test.sendMessage("page-action-shown");
} else if (msg == "page-action-set-popup") {
browser.pageAction.setPopup({popup: details.name, tabId: tabId});
browser.test.sendMessage("page-action-popup-set");
} else if (msg == "page-action-get-popup") {
browser.pageAction.getPopup({tabId: tabId}).then(url => {
browser.test.sendMessage("page-action-got-popup", url);
});
} else if (msg == "page-action-enable-onClicked-listener") {
onClickedListenerEnabled = true;
browser.test.sendMessage("page-action-onClicked-listener-enabled");
} else if (msg == "page-action-disable-onClicked-listener") {
onClickedListenerEnabled = false;
browser.test.sendMessage("page-action-onClicked-listener-disabled");
}
});
browser.pageAction.onClicked.addListener(tab => {
browser.test.assertTrue(onClickedListenerEnabled, "The onClicked listener should only fire when it is enabled.");
browser.test.sendMessage("page-action-onClicked-fired");
});
browser.test.sendMessage("ready");
}
function popupScript() {
window.onload = () => {
browser.test.sendMessage("page-action-from-popup", location.href);
};
browser.test.onMessage.addListener((msg, details) => {
if (msg == "page-action-close-popup") {
if (details.location == location.href) {
window.close();
}
}
});
}
let extension = ExtensionTestUtils.loadExtension({
background: `(${backgroundScript}())`,
manifest: {
"name": "PageAction Extension",
"page_action": {
"default_title": "Page Action",
"default_popup": "default.html",
},
},
files: {
"default.html": `<html><head><meta charset="utf-8"><script src="popup.js"></${"script"}></head></html>`,
"a.html": `<html><head><meta charset="utf-8"><script src="popup.js"></${"script"}></head></html>`,
"b.html": `<html><head><meta charset="utf-8"><script src="popup.js"></${"script"}></head></html>`,
"popup.js": `(${popupScript})()`,
},
});
let tabClosedPromise = () => {
return new Promise(resolve => {
let chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
let BrowserApp = chromeWin.BrowserApp;
let tabCloseListener = (event) => {
BrowserApp.deck.removeEventListener("TabClose", tabCloseListener, false);
let browser = event.target;
let url = browser.currentURI.spec;
resolve(url);
};
BrowserApp.deck.addEventListener("TabClose", tabCloseListener, false);
});
};
function* testPopup(name) {
// We don't need to set the popup when testing default_popup.
if (name != "default.html") {
extension.sendMessage("page-action-set-popup", {name});
yield extension.awaitMessage("page-action-popup-set");
}
extension.sendMessage("page-action-get-popup");
let url = yield extension.awaitMessage("page-action-got-popup");
if (name == "") {
ok(url == name, "Calling pageAction.getPopup should return an empty string when the popup is not set.");
// The onClicked listener should get called when the popup is set to an empty string.
extension.sendMessage("page-action-enable-onClicked-listener");
yield extension.awaitMessage("page-action-onClicked-listener-enabled");
clickPageAction(extension.id);
yield extension.awaitMessage("page-action-onClicked-fired");
extension.sendMessage("page-action-disable-onClicked-listener");
yield extension.awaitMessage("page-action-onClicked-listener-disabled");
} else {
ok(url.includes(name), "Calling pageAction.getPopup should return the correct popup URL when the popup is set.");
clickPageAction(extension.id);
let location = yield extension.awaitMessage("page-action-from-popup");
ok(location.includes(name), "The popup with the correct URL should be shown.");
extension.sendMessage("page-action-close-popup", {location});
url = yield tabClosedPromise();
ok(url.includes(name), "The tab for the popup should be closed.");
}
}
yield extension.startup();
yield extension.awaitMessage("ready");
extension.sendMessage("page-action-show");
yield extension.awaitMessage("page-action-shown");
ok(isPageActionShown(extension.id), "The PageAction should be shown.");
yield testPopup("default.html");
yield testPopup("a.html");
yield testPopup("");
yield testPopup("b.html");
yield extension.unload();
ok(!isPageActionShown(extension.id), "The PageAction should be removed after unload.");
});
</script>
</body>
</html>

View File

@ -86,32 +86,41 @@ Sanitizer.prototype = {
},
siteSettings: {
clear: function ()
{
return new Promise(function(resolve, reject) {
// Clear site-specific permissions like "Allow this site to open popups"
Services.perms.removeAll();
clear: Task.async(function* () {
// Clear site-specific permissions like "Allow this site to open popups"
Services.perms.removeAll();
// Clear site-specific settings like page-zoom level
Cc["@mozilla.org/content-pref/service;1"]
.getService(Ci.nsIContentPrefService2)
.removeAllDomains(null);
// Clear site-specific settings like page-zoom level
Cc["@mozilla.org/content-pref/service;1"]
.getService(Ci.nsIContentPrefService2)
.removeAllDomains(null);
// Clear "Never remember passwords for this site", which is not handled by
// the permission manager
var hosts = Services.logins.getAllDisabledHosts({})
for (var host of hosts) {
Services.logins.setLoginSavingEnabled(host, true);
}
// Clear "Never remember passwords for this site", which is not handled by
// the permission manager
var hosts = Services.logins.getAllDisabledHosts({})
for (var host of hosts) {
Services.logins.setLoginSavingEnabled(host, true);
}
// Clear site security settings
var sss = Cc["@mozilla.org/ssservice;1"]
.getService(Ci.nsISiteSecurityService);
sss.clearAll();
// Clear site security settings
var sss = Cc["@mozilla.org/ssservice;1"]
.getService(Ci.nsISiteSecurityService);
sss.clearAll();
resolve();
// Clear push subscriptions
yield new Promise((resolve, reject) => {
let push = Cc["@mozilla.org/push/Service;1"]
.getService(Ci.nsIPushService);
push.clearForDomain("*", status => {
if (Components.isSuccessCode(status)) {
resolve();
} else {
reject(new Error("Error clearing push subscriptions: " +
status));
}
});
});
},
}),
get canClear()
{

View File

@ -34,8 +34,7 @@ public class AboutHomeComponent extends BaseComponent {
private static final List<PanelType> PANEL_ORDERING = Arrays.asList(
PanelType.TOP_SITES,
PanelType.BOOKMARKS,
PanelType.COMBINED_HISTORY,
PanelType.RECENT_TABS
PanelType.COMBINED_HISTORY
);
// The percentage of the panel to swipe between 0 and 1. This value was set through

View File

@ -10,8 +10,6 @@ import org.mozilla.gecko.tests.helpers.GeckoHelper;
/**
* Tests functionality related to navigating between the various about:home panels.
*
* TODO: Update this test to account for recent tabs panel (bug 1028727).
*/
public class testAboutHomePageNavigation extends UITest {
// TODO: Define this test dynamically by creating dynamic representations of the Page

View File

@ -2328,7 +2328,7 @@ NS_IMETHODIMP
nsDownloadManager::OnVisit(nsIURI *aURI, int64_t aVisitID, PRTime aTime,
int64_t aSessionID, int64_t aReferringID,
uint32_t aTransitionType, const nsACString& aGUID,
bool aHidden)
bool aHidden, uint32_t aVisitCount, uint32_t aTyped)
{
return NS_OK;
}

View File

@ -1037,6 +1037,8 @@ Database::InitFunctions()
NS_ENSURE_SUCCESS(rv, rv);
rv = FrecencyNotificationFunction::create(mMainConn);
NS_ENSURE_SUCCESS(rv, rv);
rv = StoreLastInsertedIdFunction::create(mMainConn);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}

View File

@ -73,10 +73,15 @@ struct VisitData {
: placeId(0)
, visitId(0)
, hidden(true)
, shouldUpdateHidden(true)
, typed(false)
, transitionType(UINT32_MAX)
, visitTime(0)
, frecency(-1)
, lastVisitId(0)
, lastVisitTime(0)
, visitCount(0)
, referrerVisitId(0)
, titleChanged(false)
, shouldUpdateFrecency(true)
{
@ -89,10 +94,15 @@ struct VisitData {
: placeId(0)
, visitId(0)
, hidden(true)
, shouldUpdateHidden(true)
, typed(false)
, transitionType(UINT32_MAX)
, visitTime(0)
, frecency(-1)
, lastVisitId(0)
, lastVisitTime(0)
, visitCount(0)
, referrerVisitId(0)
, titleChanged(false)
, shouldUpdateFrecency(true)
{
@ -121,36 +131,20 @@ struct VisitData {
transitionType = aTransitionType;
}
/**
* Determines if this refers to the same url as aOther, and updates aOther
* with missing information if so.
*
* @param aOther
* The other place to check against.
* @return true if this is a visit for the same place as aOther, false
* otherwise.
*/
bool IsSamePlaceAs(VisitData& aOther)
{
if (!spec.Equals(aOther.spec)) {
return false;
}
aOther.placeId = placeId;
aOther.guid = guid;
return true;
}
int64_t placeId;
nsCString guid;
int64_t visitId;
nsCString spec;
nsString revHost;
bool hidden;
bool shouldUpdateHidden;
bool typed;
uint32_t transitionType;
PRTime visitTime;
int32_t frecency;
int64_t lastVisitId;
PRTime lastVisitTime;
uint32_t visitCount;
/**
* Stores the title. If this is empty (IsEmpty() returns true), then the
@ -161,6 +155,7 @@ struct VisitData {
nsString title;
nsCString referrerSpec;
int64_t referrerVisitId;
// TODO bug 626836 hook up hidden and typed change tracking too!
bool titleChanged;
@ -623,10 +618,8 @@ NS_IMPL_ISUPPORTS_INHERITED(
class NotifyVisitObservers : public Runnable
{
public:
NotifyVisitObservers(VisitData& aPlace,
VisitData& aReferrer)
explicit NotifyVisitObservers(VisitData& aPlace)
: mPlace(aPlace)
, mReferrer(aReferrer)
, mHistory(History::GetService())
{
}
@ -657,8 +650,10 @@ public:
// to the database, thus cannot be queried and we don't notify them.
if (mPlace.transitionType != nsINavHistoryService::TRANSITION_EMBED) {
navHistory->NotifyOnVisit(uri, mPlace.visitId, mPlace.visitTime,
mReferrer.visitId, mPlace.transitionType,
mPlace.guid, mPlace.hidden);
mPlace.referrerVisitId, mPlace.transitionType,
mPlace.guid, mPlace.hidden,
mPlace.visitCount + 1, // Add current visit.
static_cast<uint32_t>(mPlace.typed));
}
nsCOMPtr<nsIObserverService> obsService =
@ -678,7 +673,6 @@ public:
}
private:
VisitData mPlace;
VisitData mReferrer;
RefPtr<History> mHistory;
};
@ -927,7 +921,6 @@ public:
VisitData* lastFetchedPlace = nullptr;
for (nsTArray<VisitData>::size_type i = 0; i < mPlaces.Length(); i++) {
VisitData& place = mPlaces.ElementAt(i);
VisitData& referrer = mReferrers.ElementAt(i);
// Fetching from the database can overwrite this information, so save it
// apart.
@ -936,7 +929,7 @@ public:
// We can avoid a database lookup if it's the same place as the last
// visit we added.
bool known = lastFetchedPlace && lastFetchedPlace->IsSamePlaceAs(place);
bool known = lastFetchedPlace && lastFetchedPlace->spec.Equals(place.spec);
if (!known) {
nsresult rv = mHistory->FetchPageInfo(place, &known);
if (NS_FAILED(rv)) {
@ -948,6 +941,16 @@ public:
return NS_OK;
}
lastFetchedPlace = &mPlaces.ElementAt(i);
} else {
// Copy over the data from the already known place.
place.placeId = lastFetchedPlace->placeId;
place.guid = lastFetchedPlace->guid;
place.lastVisitId = lastFetchedPlace->visitId;
place.lastVisitTime = lastFetchedPlace->visitTime;
place.titleChanged = !lastFetchedPlace->title.Equals(place.title);
place.frecency = lastFetchedPlace->frecency;
// Add one visit for the previous loop.
place.visitCount = ++(*lastFetchedPlace).visitCount;
}
// If any transition is typed, ensure the page is marked as typed.
@ -960,9 +963,15 @@ public:
place.hidden = false;
}
FetchReferrerInfo(referrer, place);
// If this is a new page, or the existing page was already visible,
// there's no need to try to unhide it.
if (!known || !lastFetchedPlace->hidden) {
place.shouldUpdateHidden = false;
}
nsresult rv = DoDatabaseInserts(known, place, referrer);
FetchReferrerInfo(place);
nsresult rv = DoDatabaseInserts(known, place);
if (!!mCallback) {
nsCOMPtr<nsIRunnable> event =
new NotifyPlaceInfoCallback(mCallback, place, true, rv);
@ -971,7 +980,7 @@ public:
}
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<nsIRunnable> event = new NotifyVisitObservers(place, referrer);
nsCOMPtr<nsIRunnable> event = new NotifyVisitObservers(place);
rv = NS_DispatchToMainThread(event);
NS_ENSURE_SUCCESS(rv, rv);
@ -999,18 +1008,15 @@ private:
MOZ_ASSERT(NS_IsMainThread(), "This should be called on the main thread");
mPlaces.SwapElements(aPlaces);
mReferrers.SetLength(mPlaces.Length());
for (nsTArray<VisitData>::size_type i = 0; i < mPlaces.Length(); i++) {
mReferrers[i].spec = mPlaces[i].referrerSpec;
#ifdef DEBUG
for (nsTArray<VisitData>::size_type i = 0; i < mPlaces.Length(); i++) {
nsCOMPtr<nsIURI> uri;
MOZ_ASSERT(NS_SUCCEEDED(NS_NewURI(getter_AddRefs(uri), mPlaces[i].spec)));
NS_ASSERTION(CanAddURI(uri),
"Passed a VisitData with a URI we cannot add to history!");
#endif
MOZ_ASSERT(CanAddURI(uri),
"Passed a VisitData with a URI we cannot add to history!");
}
#endif
}
/**
@ -1022,12 +1028,9 @@ private:
* otherwise.
* @param aPlace
* The place we are adding a visit for.
* @param aReferrer
* The referrer for aPlace.
*/
nsresult DoDatabaseInserts(bool aKnown,
VisitData& aPlace,
VisitData& aReferrer)
VisitData& aPlace)
{
MOZ_ASSERT(!NS_IsMainThread(), "This should not be called on the main thread");
@ -1041,22 +1044,11 @@ private:
else {
rv = mHistory->InsertPlace(aPlace);
NS_ENSURE_SUCCESS(rv, rv);
// We need the place id and guid of the page we just inserted when we
// have a callback or when the GUID isn't known. No point in doing the
// disk I/O if we do not need it.
if (!!mCallback || aPlace.guid.IsEmpty()) {
bool exists;
rv = mHistory->FetchPageInfo(aPlace, &exists);
NS_ENSURE_SUCCESS(rv, rv);
if (!exists) {
NS_NOTREACHED("should have an entry in moz_places");
}
}
aPlace.placeId = nsNavHistory::sLastInsertedPlaceId;
}
MOZ_ASSERT(aPlace.placeId > 0);
rv = AddVisit(aPlace, aReferrer);
rv = AddVisit(aPlace);
NS_ENSURE_SUCCESS(rv, rv);
// TODO (bug 623969) we shouldn't update this after each visit, but
@ -1071,102 +1063,42 @@ private:
return NS_OK;
}
/**
* Loads visit information about the page into _place.
*
* @param _place
* The VisitData for the place we need to know visit information about.
* @param [optional] aThresholdStart
* The timestamp of a new visit (not represented by _place) used to
* determine if the page was recently visited or not.
* @return true if the page was recently (determined with aThresholdStart)
* visited, false otherwise.
*/
bool FetchVisitInfo(VisitData& _place,
PRTime aThresholdStart = 0)
{
NS_PRECONDITION(!_place.spec.IsEmpty(), "must have a non-empty spec!");
nsCOMPtr<mozIStorageStatement> stmt;
// If we have a visitTime, we want information on that specific visit.
if (_place.visitTime) {
stmt = mHistory->GetStatement(
"SELECT id, visit_date "
"FROM moz_historyvisits "
"WHERE place_id = (SELECT id FROM moz_places WHERE url = :page_url) "
"AND visit_date = :visit_date "
);
NS_ENSURE_TRUE(stmt, false);
mozStorageStatementScoper scoper(stmt);
nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("visit_date"),
_place.visitTime);
NS_ENSURE_SUCCESS(rv, false);
scoper.Abandon();
}
// Otherwise, we want information about the most recent visit.
else {
stmt = mHistory->GetStatement(
"SELECT id, visit_date "
"FROM moz_historyvisits "
"WHERE place_id = (SELECT id FROM moz_places WHERE url = :page_url) "
"ORDER BY visit_date DESC "
);
NS_ENSURE_TRUE(stmt, false);
}
mozStorageStatementScoper scoper(stmt);
nsresult rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"),
_place.spec);
NS_ENSURE_SUCCESS(rv, false);
bool hasResult;
rv = stmt->ExecuteStep(&hasResult);
NS_ENSURE_SUCCESS(rv, false);
if (!hasResult) {
return false;
}
rv = stmt->GetInt64(0, &_place.visitId);
NS_ENSURE_SUCCESS(rv, false);
rv = stmt->GetInt64(1, reinterpret_cast<int64_t*>(&_place.visitTime));
NS_ENSURE_SUCCESS(rv, false);
// If we have been given a visit threshold start time, go ahead and
// calculate if we have been recently visited.
if (aThresholdStart &&
aThresholdStart - _place.visitTime <= RECENT_EVENT_THRESHOLD) {
return true;
}
return false;
}
/**
* Fetches information about a referrer for aPlace if it was a recent
* visit or not.
*
* @param aReferrer
* The VisitData for the referrer. This will be populated with
* FetchVisitInfo.
* @param aPlace
* The VisitData for the visit we will eventually add.
*
*/
void FetchReferrerInfo(VisitData& aReferrer,
VisitData& aPlace)
void FetchReferrerInfo(VisitData& aPlace)
{
if (aReferrer.spec.IsEmpty()) {
if (aPlace.referrerSpec.IsEmpty()) {
return;
}
if (!FetchVisitInfo(aReferrer, aPlace.visitTime)) {
// We must change both the place and referrer to indicate that we will
// not be using the referrer's data. This behavior has test coverage, so
// if this invariant changes, we'll know.
VisitData referrer;
referrer.spec = aPlace.referrerSpec;
// If the referrer is the same as the page, we don't need to fetch it.
if (aPlace.referrerSpec.Equals(aPlace.spec)) {
referrer = aPlace;
// The page last visit id is also the referrer visit id.
aPlace.referrerVisitId = aPlace.lastVisitId;
} else {
bool exists = false;
if (NS_SUCCEEDED(mHistory->FetchPageInfo(referrer, &exists)) && exists) {
// Copy the referrer last visit id.
aPlace.referrerVisitId = referrer.lastVisitId;
}
}
// Check if the page has effectively been visited recently, otherwise
// discard the referrer info.
if (!aPlace.referrerVisitId || !referrer.lastVisitTime ||
aPlace.visitTime - referrer.lastVisitTime > RECENT_EVENT_THRESHOLD) {
// We will not be using the referrer data.
aPlace.referrerSpec.Truncate();
aReferrer.visitId = 0;
aPlace.referrerVisitId = 0;
}
}
@ -1175,36 +1107,25 @@ private:
*
* @param _place
* The VisitData for the place we need to know visit information about.
* @param aReferrer
* A reference to the referrer's visit data.
*/
nsresult AddVisit(VisitData& _place,
const VisitData& aReferrer)
nsresult AddVisit(VisitData& _place)
{
MOZ_ASSERT(_place.placeId > 0);
nsresult rv;
nsCOMPtr<mozIStorageStatement> stmt;
if (_place.placeId) {
stmt = mHistory->GetStatement(
"INSERT INTO moz_historyvisits "
"(from_visit, place_id, visit_date, visit_type, session) "
"VALUES (:from_visit, :page_id, :visit_date, :visit_type, 0) "
);
NS_ENSURE_STATE(stmt);
rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), _place.placeId);
NS_ENSURE_SUCCESS(rv, rv);
}
else {
stmt = mHistory->GetStatement(
"INSERT INTO moz_historyvisits "
"(from_visit, place_id, visit_date, visit_type, session) "
"VALUES (:from_visit, (SELECT id FROM moz_places WHERE url = :page_url), :visit_date, :visit_type, 0) "
);
NS_ENSURE_STATE(stmt);
rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), _place.spec);
NS_ENSURE_SUCCESS(rv, rv);
}
stmt = mHistory->GetStatement(
"INSERT INTO moz_historyvisits "
"(from_visit, place_id, visit_date, visit_type, session) "
"VALUES (:from_visit, :page_id, :visit_date, :visit_type, 0) "
);
NS_ENSURE_STATE(stmt);
mozStorageStatementScoper scoper(stmt);
rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), _place.placeId);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("from_visit"),
aReferrer.visitId);
_place.referrerVisitId);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("visit_date"),
_place.visitTime);
@ -1217,13 +1138,11 @@ private:
transitionType);
NS_ENSURE_SUCCESS(rv, rv);
mozStorageStatementScoper scoper(stmt);
rv = stmt->Execute();
NS_ENSURE_SUCCESS(rv, rv);
// Now that it should be in the database, we need to obtain the id of the
// visit we just added.
(void)FetchVisitInfo(_place);
_place.visitId = nsNavHistory::sLastInsertedVisitId;
MOZ_ASSERT(_place.visitId > 0);
return NS_OK;
}
@ -1237,66 +1156,43 @@ private:
nsresult UpdateFrecency(const VisitData& aPlace)
{
MOZ_ASSERT(aPlace.shouldUpdateFrecency);
MOZ_ASSERT(aPlace.placeId > 0);
nsresult rv;
{ // First, set our frecency to the proper value.
nsCOMPtr<mozIStorageStatement> stmt;
if (aPlace.placeId) {
stmt = mHistory->GetStatement(
"UPDATE moz_places "
"SET frecency = NOTIFY_FRECENCY("
"CALCULATE_FRECENCY(:page_id), "
"url, guid, hidden, last_visit_date"
") "
"WHERE id = :page_id"
);
NS_ENSURE_STATE(stmt);
rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), aPlace.placeId);
NS_ENSURE_SUCCESS(rv, rv);
}
else {
stmt = mHistory->GetStatement(
"UPDATE moz_places "
"SET frecency = NOTIFY_FRECENCY("
"CALCULATE_FRECENCY(id), url, guid, hidden, last_visit_date"
") "
"WHERE url = :page_url"
);
NS_ENSURE_STATE(stmt);
rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aPlace.spec);
NS_ENSURE_SUCCESS(rv, rv);
}
stmt = mHistory->GetStatement(
"UPDATE moz_places "
"SET frecency = NOTIFY_FRECENCY("
"CALCULATE_FRECENCY(:page_id), "
"url, guid, hidden, last_visit_date"
") "
"WHERE id = :page_id"
);
NS_ENSURE_STATE(stmt);
mozStorageStatementScoper scoper(stmt);
rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), aPlace.placeId);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->Execute();
NS_ENSURE_SUCCESS(rv, rv);
}
if (!aPlace.hidden) {
if (!aPlace.hidden && aPlace.shouldUpdateHidden) {
// Mark the page as not hidden if the frecency is now nonzero.
nsCOMPtr<mozIStorageStatement> stmt;
if (aPlace.placeId) {
stmt = mHistory->GetStatement(
"UPDATE moz_places "
"SET hidden = 0 "
"WHERE id = :page_id AND frecency <> 0"
);
NS_ENSURE_STATE(stmt);
rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), aPlace.placeId);
NS_ENSURE_SUCCESS(rv, rv);
}
else {
stmt = mHistory->GetStatement(
"UPDATE moz_places "
"SET hidden = 0 "
"WHERE url = :page_url AND frecency <> 0"
);
NS_ENSURE_STATE(stmt);
rv = URIBinder::Bind(stmt, NS_LITERAL_CSTRING("page_url"), aPlace.spec);
NS_ENSURE_SUCCESS(rv, rv);
}
stmt = mHistory->GetStatement(
"UPDATE moz_places "
"SET hidden = 0 "
"WHERE id = :page_id AND frecency <> 0"
);
NS_ENSURE_STATE(stmt);
mozStorageStatementScoper scoper(stmt);
rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("page_id"), aPlace.placeId);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->Execute();
NS_ENSURE_SUCCESS(rv, rv);
}
@ -1307,7 +1203,6 @@ private:
mozIStorageConnection* mDBConn;
nsTArray<VisitData> mPlaces;
nsTArray<VisitData> mReferrers;
nsMainThreadPtrHandle<mozIVisitInfoCallback> mCallback;
@ -1428,8 +1323,8 @@ public:
return NS_OK;
}
NS_ASSERTION(mPlace.placeId > 0,
"We somehow have an invalid place id here!");
MOZ_ASSERT(mPlace.placeId > 0,
"We somehow have an invalid place id here!");
// Now we can update our database record.
nsCOMPtr<mozIStorageStatement> stmt =
@ -1954,8 +1849,7 @@ StoreAndNotifyEmbedVisit(VisitData& aPlace,
(void)NS_DispatchToMainThread(event);
}
VisitData noReferrer;
nsCOMPtr<nsIRunnable> event = new NotifyVisitObservers(aPlace, noReferrer);
nsCOMPtr<nsIRunnable> event = new NotifyVisitObservers(aPlace);
(void)NS_DispatchToMainThread(event);
}
@ -2135,10 +2029,11 @@ History::GetIsVisitedStatement(mozIStorageCompletionCallback* aCallback)
}
nsresult
History::InsertPlace(const VisitData& aPlace)
History::InsertPlace(VisitData& aPlace)
{
NS_PRECONDITION(aPlace.placeId == 0, "should not have a valid place id!");
NS_PRECONDITION(!NS_IsMainThread(), "must be called off of the main thread!");
MOZ_ASSERT(aPlace.placeId == 0, "should not have a valid place id!");
MOZ_ASSERT(!aPlace.shouldUpdateHidden, "We should not need to update hidden");
MOZ_ASSERT(!NS_IsMainThread(), "must be called off of the main thread!");
nsCOMPtr<mozIStorageStatement> stmt = GetStatement(
"INSERT INTO moz_places "
@ -2172,12 +2067,11 @@ History::InsertPlace(const VisitData& aPlace)
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("hidden"), aPlace.hidden);
NS_ENSURE_SUCCESS(rv, rv);
nsAutoCString guid(aPlace.guid);
if (aPlace.guid.IsVoid()) {
rv = GenerateGUID(guid);
rv = GenerateGUID(aPlace.guid);
NS_ENSURE_SUCCESS(rv, rv);
}
rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), guid);
rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), aPlace.guid);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->Execute();
NS_ENSURE_SUCCESS(rv, rv);
@ -2185,7 +2079,8 @@ History::InsertPlace(const VisitData& aPlace)
// Post an onFrecencyChanged observer notification.
const nsNavHistory* navHistory = nsNavHistory::GetConstHistoryService();
NS_ENSURE_STATE(navHistory);
navHistory->DispatchFrecencyChangedNotification(aPlace.spec, frecency, guid,
navHistory->DispatchFrecencyChangedNotification(aPlace.spec, frecency,
aPlace.guid,
aPlace.hidden,
aPlace.visitTime);
@ -2195,9 +2090,9 @@ History::InsertPlace(const VisitData& aPlace)
nsresult
History::UpdatePlace(const VisitData& aPlace)
{
NS_PRECONDITION(!NS_IsMainThread(), "must be called off of the main thread!");
NS_PRECONDITION(aPlace.placeId > 0, "must have a valid place id!");
NS_PRECONDITION(!aPlace.guid.IsVoid(), "must have a guid!");
MOZ_ASSERT(!NS_IsMainThread(), "must be called off of the main thread!");
MOZ_ASSERT(aPlace.placeId > 0, "must have a valid place id!");
MOZ_ASSERT(!aPlace.guid.IsVoid(), "must have a guid!");
nsCOMPtr<mozIStorageStatement> stmt = GetStatement(
"UPDATE moz_places "
@ -2238,8 +2133,8 @@ History::UpdatePlace(const VisitData& aPlace)
nsresult
History::FetchPageInfo(VisitData& _place, bool* _exists)
{
NS_PRECONDITION(!_place.spec.IsEmpty() || !_place.guid.IsEmpty(), "must have either a non-empty spec or guid!");
NS_PRECONDITION(!NS_IsMainThread(), "must be called off of the main thread!");
MOZ_ASSERT(!_place.spec.IsEmpty() || !_place.guid.IsEmpty(), "must have either a non-empty spec or guid!");
MOZ_ASSERT(!NS_IsMainThread(), "must be called off of the main thread!");
nsresult rv;
@ -2248,8 +2143,10 @@ History::FetchPageInfo(VisitData& _place, bool* _exists)
bool selectByURI = !_place.spec.IsEmpty();
if (selectByURI) {
stmt = GetStatement(
"SELECT guid, id, title, hidden, typed, frecency "
"FROM moz_places "
"SELECT guid, id, title, hidden, typed, frecency, visit_count, last_visit_date, "
"(SELECT id FROM moz_historyvisits "
"WHERE place_id = h.id AND visit_date = h.last_visit_date) AS last_visit_id "
"FROM moz_places h "
"WHERE url = :page_url "
);
NS_ENSURE_STATE(stmt);
@ -2259,8 +2156,10 @@ History::FetchPageInfo(VisitData& _place, bool* _exists)
}
else {
stmt = GetStatement(
"SELECT url, id, title, hidden, typed, frecency "
"FROM moz_places "
"SELECT url, id, title, hidden, typed, frecency, visit_count, last_visit_date, "
"(SELECT id FROM moz_historyvisits "
"WHERE place_id = h.id AND visit_date = h.last_visit_date) AS last_visit_id "
"FROM moz_places h "
"WHERE guid = :guid "
);
NS_ENSURE_STATE(stmt);
@ -2323,6 +2222,15 @@ History::FetchPageInfo(VisitData& _place, bool* _exists)
rv = stmt->GetInt32(5, &_place.frecency);
NS_ENSURE_SUCCESS(rv, rv);
int32_t visitCount;
rv = stmt->GetInt32(6, &visitCount);
NS_ENSURE_SUCCESS(rv, rv);
_place.visitCount = visitCount;
rv = stmt->GetInt64(7, &_place.lastVisitTime);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->GetInt64(8, &_place.lastVisitId);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}

View File

@ -69,7 +69,7 @@ public:
* @param aVisitData
* The visit data to use to populate a new row in moz_places.
*/
nsresult InsertPlace(const VisitData& aVisitData);
nsresult InsertPlace(VisitData& aVisitData);
/**
* Updates an entry in moz_places with the data in aVisitData.

View File

@ -844,5 +844,55 @@ namespace places {
return NS_OK;
}
////////////////////////////////////////////////////////////////////////////////
//// Store Last Inserted Id Function
StoreLastInsertedIdFunction::~StoreLastInsertedIdFunction()
{
}
/* static */
nsresult
StoreLastInsertedIdFunction::create(mozIStorageConnection *aDBConn)
{
RefPtr<StoreLastInsertedIdFunction> function =
new StoreLastInsertedIdFunction();
nsresult rv = aDBConn->CreateFunction(
NS_LITERAL_CSTRING("store_last_inserted_id"), 2, function
);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
NS_IMPL_ISUPPORTS(
StoreLastInsertedIdFunction,
mozIStorageFunction
)
NS_IMETHODIMP
StoreLastInsertedIdFunction::OnFunctionCall(mozIStorageValueArray *aArgs,
nsIVariant **_result)
{
uint32_t numArgs;
nsresult rv = aArgs->GetNumEntries(&numArgs);
NS_ENSURE_SUCCESS(rv, rv);
MOZ_ASSERT(numArgs == 2);
nsAutoCString table;
rv = aArgs->GetUTF8String(0, table);
NS_ENSURE_SUCCESS(rv, rv);
int64_t lastInsertedId = aArgs->AsInt64(1);
nsNavHistory::StoreLastInsertedId(table, lastInsertedId);
RefPtr<nsVariant> result = new nsVariant();
rv = result->SetAsInt64(lastInsertedId);
NS_ENSURE_SUCCESS(rv, rv);
result.forget(_result);
return NS_OK;
}
} // namespace places
} // namespace mozilla

View File

@ -324,6 +324,34 @@ public:
};
////////////////////////////////////////////////////////////////////////////////
//// Store Last Inserted Id Function
/**
* Store the last inserted id for reference purpose.
*
* @param tableName
* The table name.
* @param id
* The last inserted id.
* @return null
*/
class StoreLastInsertedIdFunction final : public mozIStorageFunction
{
~StoreLastInsertedIdFunction();
public:
NS_DECL_THREADSAFE_ISUPPORTS
NS_DECL_MOZISTORAGEFUNCTION
/**
* Registers the function with the specified database connection.
*
* @param aDBConn
* The database connection to register with.
*/
static nsresult create(mozIStorageConnection *aDBConn);
};
} // namespace places
} // namespace mozilla

View File

@ -644,30 +644,44 @@ interface nsINavHistoryObserver : nsISupports
void onEndUpdateBatch();
/**
* Called when a resource is visited. This is called the first time a
* resource (page, image, etc.) is seen as well as every subsequent time.
* Called everytime a URI is visited.
*
* Normally, transition types of TRANSITION_EMBED (corresponding to images in
* a page, for example) are not displayed in history results (unless
* includeHidden is set). Many observers can ignore _EMBED notifications
* (which will comprise the majority of visit notifications) to save work.
* @note TRANSITION_EMBED visits (corresponding to images in a page, for
* example) are not displayed in history results. Most observers can
* ignore TRANSITION_EMBED visit notifications (which will comprise the
* majority of visit notifications) to save work.
*
* @param aVisitID ID of the visit that was just created.
* @param aTime Time of the visit
* @param aSessionID No longer supported (always set to 0).
* @param aReferringID The ID of the visit the user came from. 0 if empty.
* @param aTransitionType One of nsINavHistory.TRANSITION_*
* @param aGUID The unique ID associated with the page.
* @param aHidden Whether the visited page is marked as hidden.
* @param aVisitId
* Id of the visit that was just created.
* @param aTime
* Time of the visit.
* @param aSessionId
* No longer supported and always set to 0.
* @param aReferrerVisitId
* The id of the visit the user came from, defaults to 0 for no referrer.
* @param aTransitionType
* One of nsINavHistory.TRANSITION_*
* @param aGuid
* The unique id associated with the page.
* @param aHidden
* Whether the visited page is marked as hidden.
* @param aVisitCount
* Number of visits (included this one) for this URI.
* @param aTyped
* Whether the URI has been typed or not.
* TODO (Bug 1271801): This will become a count, rather than a boolean.
* For future compatibility, always compare it with "> 0".
*/
void onVisit(in nsIURI aURI,
in long long aVisitID,
in long long aVisitId,
in PRTime aTime,
in long long aSessionID,
in long long aReferringID,
in long long aSessionId,
in long long aReferrerVisitId,
in unsigned long aTransitionType,
in ACString aGUID,
in boolean aHidden);
in ACString aGuid,
in boolean aHidden,
in unsigned long aVisitCount,
in unsigned long aTyped);
/**
* Called whenever either the "real" title or the custom title of the page

View File

@ -2673,7 +2673,7 @@ NS_IMETHODIMP
nsNavBookmarks::OnVisit(nsIURI* aURI, int64_t aVisitId, PRTime aTime,
int64_t aSessionID, int64_t aReferringID,
uint32_t aTransitionType, const nsACString& aGUID,
bool aHidden)
bool aHidden, uint32_t aVisitCount, uint32_t aTyped)
{
NS_ENSURE_ARG(aURI);

View File

@ -405,8 +405,8 @@ nsNavHistory::GetOrCreateIdForPage(nsIURI* aURI,
// Create a new hidden, untyped and unvisited entry.
nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
"INSERT OR IGNORE INTO moz_places (url, rev_host, hidden, frecency, guid) "
"VALUES (:page_url, :rev_host, :hidden, :frecency, GENERATE_GUID()) "
"INSERT INTO moz_places (url, rev_host, hidden, frecency, guid) "
"VALUES (:page_url, :rev_host, :hidden, :frecency, :guid) "
);
NS_ENSURE_STATE(stmt);
mozStorageStatementScoper scoper(stmt);
@ -431,28 +431,16 @@ nsNavHistory::GetOrCreateIdForPage(nsIURI* aURI,
rv = stmt->BindInt32ByName(NS_LITERAL_CSTRING("frecency"),
IsQueryURI(spec) ? 0 : -1);
NS_ENSURE_SUCCESS(rv, rv);
nsAutoCString guid;
rv = GenerateGUID(_GUID);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->BindUTF8StringByName(NS_LITERAL_CSTRING("guid"), _GUID);
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->Execute();
NS_ENSURE_SUCCESS(rv, rv);
{
nsCOMPtr<mozIStorageStatement> getIdStmt = mDB->GetStatement(
"SELECT id, guid FROM moz_places WHERE url = :page_url "
);
NS_ENSURE_STATE(getIdStmt);
mozStorageStatementScoper getIdScoper(getIdStmt);
rv = URIBinder::Bind(getIdStmt, NS_LITERAL_CSTRING("page_url"), aURI);
NS_ENSURE_SUCCESS(rv, rv);
bool hasResult = false;
rv = getIdStmt->ExecuteStep(&hasResult);
NS_ENSURE_SUCCESS(rv, rv);
NS_ASSERTION(hasResult, "hasResult is false but the call succeeded?");
*_pageId = getIdStmt->AsInt64(0);
rv = getIdStmt->GetUTF8String(1, _GUID);
NS_ENSURE_SUCCESS(rv, rv);
}
*_pageId = sLastInsertedPlaceId;
return NS_OK;
}
@ -497,14 +485,16 @@ nsNavHistory::LoadPrefs()
void
nsNavHistory::NotifyOnVisit(nsIURI* aURI,
int64_t aVisitID,
int64_t aVisitId,
PRTime aTime,
int64_t referringVisitID,
int64_t aReferrerVisitId,
int32_t aTransitionType,
const nsACString& aGUID,
bool aHidden)
const nsACString& aGuid,
bool aHidden,
uint32_t aVisitCount,
uint32_t aTyped)
{
MOZ_ASSERT(!aGUID.IsEmpty());
MOZ_ASSERT(!aGuid.IsEmpty());
// If there's no history, this visit will surely add a day. If the visit is
// added before or after the last cached day, the day count may have changed.
// Otherwise adding multiple visits in the same day should not invalidate
@ -517,8 +507,8 @@ nsNavHistory::NotifyOnVisit(nsIURI* aURI,
NOTIFY_OBSERVERS(mCanNotify, mCacheObservers, mObservers,
nsINavHistoryObserver,
OnVisit(aURI, aVisitID, aTime, 0,
referringVisitID, aTransitionType, aGUID, aHidden));
OnVisit(aURI, aVisitId, aTime, 0, aReferrerVisitId,
aTransitionType, aGuid, aHidden, aVisitCount, aTyped));
}
void
@ -613,6 +603,21 @@ nsNavHistory::DispatchFrecencyChangedNotification(const nsACString& aSpec,
(void)NS_DispatchToMainThread(notif);
}
Atomic<int64_t> nsNavHistory::sLastInsertedPlaceId(0);
Atomic<int64_t> nsNavHistory::sLastInsertedVisitId(0);
void // static
nsNavHistory::StoreLastInsertedId(const nsACString& aTable,
const int64_t aLastInsertedId) {
if (aTable.Equals(NS_LITERAL_CSTRING("moz_places"))) {
nsNavHistory::sLastInsertedPlaceId = aLastInsertedId;
} else if (aTable.Equals(NS_LITERAL_CSTRING("moz_historyvisits"))) {
nsNavHistory::sLastInsertedVisitId = aLastInsertedId;
} else {
MOZ_ASSERT(false, "Trying to store the insert id for an unknown table?");
}
}
int32_t
nsNavHistory::GetDaysOfHistory() {
MOZ_ASSERT(NS_IsMainThread(), "This can only be called on the main thread");

View File

@ -27,6 +27,7 @@
#include "nsNavHistoryQuery.h"
#include "Database.h"
#include "mozilla/Attributes.h"
#include "mozilla/Atomics.h"
#define QUERYUPDATE_TIME 0
#define QUERYUPDATE_SIMPLE 1
@ -430,12 +431,14 @@ public:
* Fires onVisit event to nsINavHistoryService observers
*/
void NotifyOnVisit(nsIURI* aURI,
int64_t aVisitID,
int64_t aVisitId,
PRTime aTime,
int64_t referringVisitID,
int64_t aReferrerVisitId,
int32_t aTransitionType,
const nsACString& aGUID,
bool aHidden);
const nsACString& aGuid,
bool aHidden,
uint32_t aVisitCount,
uint32_t aTyped);
/**
* Fires onTitleChanged event to nsINavHistoryService observers
@ -467,6 +470,15 @@ public:
bool aHidden,
PRTime aLastVisitDate) const;
/**
* Store last insterted id for a table.
*/
static mozilla::Atomic<int64_t> sLastInsertedPlaceId;
static mozilla::Atomic<int64_t> sLastInsertedVisitId;
static void StoreLastInsertedId(const nsACString& aTable,
const int64_t aLastInsertedId);
bool isBatching() {
return mBatchLevel > 0;
}

View File

@ -4597,10 +4597,15 @@ NS_IMETHODIMP
nsNavHistoryResult::OnVisit(nsIURI* aURI, int64_t aVisitId, PRTime aTime,
int64_t aSessionId, int64_t aReferringId,
uint32_t aTransitionType, const nsACString& aGUID,
bool aHidden)
bool aHidden, uint32_t aVisitCount, uint32_t aTyped)
{
NS_ENSURE_ARG(aURI);
// Embed visits are never shown in our views.
if (aTransitionType == nsINavHistoryService::TRANSITION_EMBED) {
return NS_OK;
}
uint32_t added = 0;
ENUMERATE_HISTORY_OBSERVERS(OnVisit(aURI, aVisitId, aTime, aSessionId,

View File

@ -94,7 +94,7 @@ private:
NS_IMETHOD OnVisit(nsIURI* aURI, int64_t aVisitId, PRTime aTime, \
int64_t aSessionId, int64_t aReferringId, \
uint32_t aTransitionType, const nsACString& aGUID, \
bool aHidden) __VA_ARGS__;
bool aHidden, uint32_t aVisitCount, uint32_t aTyped) __VA_ARGS__;
// nsNavHistoryResult
//

View File

@ -27,6 +27,7 @@
"CREATE TEMP TRIGGER moz_historyvisits_afterinsert_v2_trigger " \
"AFTER INSERT ON moz_historyvisits FOR EACH ROW " \
"BEGIN " \
"SELECT store_last_inserted_id('moz_historyvisits', NEW.id); " \
"UPDATE moz_places SET " \
"visit_count = visit_count + (SELECT NEW.visit_type NOT IN (" EXCLUDED_VISIT_TYPES ")), "\
"last_visit_date = MAX(IFNULL(last_visit_date, 0), NEW.visit_date) " \
@ -94,20 +95,20 @@
#define CREATE_PLACES_AFTERINSERT_TRIGGER NS_LITERAL_CSTRING( \
"CREATE TEMP TRIGGER moz_places_afterinsert_trigger " \
"AFTER INSERT ON moz_places FOR EACH ROW " \
"WHEN LENGTH(NEW.rev_host) > 1 " \
"BEGIN " \
"SELECT store_last_inserted_id('moz_places', NEW.id); " \
"INSERT OR REPLACE INTO moz_hosts (id, host, frecency, typed, prefix) " \
"VALUES (" \
"(SELECT id FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), " \
"fixup_url(get_unreversed_host(NEW.rev_host)), " \
"MAX(IFNULL((SELECT frecency FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), -1), NEW.frecency), " \
"MAX(IFNULL((SELECT typed FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), 0), NEW.typed), " \
"(" HOSTS_PREFIX_PRIORITY_FRAGMENT \
"FROM ( " \
"SELECT fixup_url(get_unreversed_host(NEW.rev_host)) AS host " \
") AS match " \
") " \
"); " \
"SELECT " \
"(SELECT id FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), " \
"fixup_url(get_unreversed_host(NEW.rev_host)), " \
"MAX(IFNULL((SELECT frecency FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), -1), NEW.frecency), " \
"MAX(IFNULL((SELECT typed FROM moz_hosts WHERE host = fixup_url(get_unreversed_host(NEW.rev_host))), 0), NEW.typed), " \
"(" HOSTS_PREFIX_PRIORITY_FRAGMENT \
"FROM ( " \
"SELECT fixup_url(get_unreversed_host(NEW.rev_host)) AS host " \
") AS match " \
") " \
" WHERE LENGTH(NEW.rev_host) > 1; " \
"END" \
)

View File

@ -57,11 +57,16 @@ this.PlacesTestUtils = Object.freeze({
if (typeof place.uri == "string") {
place.uri = NetUtil.newURI(place.uri);
} else if (place.uri instanceof URL) {
place.uri = NetUtil.newURI(place.href);
place.uri = NetUtil.newURI(place.uri.href);
}
if (typeof place.title != "string") {
place.title = "test visit for " + place.uri.spec;
}
if (typeof place.referrer == "string") {
place.referrer = NetUtil.newURI(place.referrer);
} else if (place.referrer instanceof URL) {
place.referrer = NetUtil.newURI(place.referrer.href);
}
place.visits = [{
transitionType: place.transition === undefined ? Ci.nsINavHistoryService.TRANSITION_LINK
: place.transition,

View File

@ -0,0 +1,52 @@
// Test that repeated additions of the same URI through updatePlaces, properly
// update from_visit and notify titleChanged.
add_task(function* test() {
let uri = "http://test.com/";
let promiseTitleChangedNotifications = new Promise(resolve => {
let historyObserver = {
_count: 0,
__proto__: NavHistoryObserver.prototype,
onTitleChanged(aURI, aTitle, aGUID) {
Assert.equal(aURI.spec, uri, "Should notify the proper url");
if (++this._count == 2) {
PlacesUtils.history.removeObserver(historyObserver);
resolve();
}
}
};
PlacesUtils.history.addObserver(historyObserver, false);
});
// This repeats the url on purpose, don't merge it into a single place entry.
yield PlacesTestUtils.addVisits([
{ uri, title: "test" },
{ uri, referrer: uri, title: "test2" },
]);
let options = PlacesUtils.history.getNewQueryOptions();
let query = PlacesUtils.history.getNewQuery();
query.uri = NetUtil.newURI(uri);
options.resultType = options.RESULTS_AS_VISIT;
let root = PlacesUtils.history.executeQuery(query, options).root;
root.containerOpen = true;
Assert.equal(root.childCount, 2);
let child = root.getChild(0);
Assert.equal(child.visitType, TRANSITION_LINK, "Visit type should be TRANSITION_LINK");
Assert.equal(child.visitId, 1, "Visit ID should be 1");
Assert.equal(child.fromVisitId, -1, "Should have no referrer visit ID");
Assert.equal(child.title, "test2", "Should have the correct title");
child = root.getChild(1);
Assert.equal(child.visitType, TRANSITION_LINK, "Visit type should be TRANSITION_LINK");
Assert.equal(child.visitId, 2, "Visit ID should be 2");
Assert.equal(child.fromVisitId, 1, "First visit should be the referring visit");
Assert.equal(child.title, "test2", "Should have the correct title");
root.containerOpen = false;
yield promiseTitleChangedNotifications;
});

View File

@ -6,3 +6,4 @@ tail =
[test_remove.js]
[test_removeVisits.js]
[test_removeVisitsByFilter.js]
[test_updatePlaces_sameUri_titleChanged.js]

View File

@ -25,15 +25,15 @@ NavHistoryObserver.prototype = {
* Returns a promise that is resolved when the callback returns.
*/
function onNotify(callback) {
let deferred = Promise.defer();
let obs = new NavHistoryObserver();
obs[callback.name] = function () {
PlacesUtils.history.removeObserver(this);
callback.apply(this, arguments);
deferred.resolve();
};
PlacesUtils.history.addObserver(obs, false);
return deferred.promise;
return new Promise(resolve => {
let obs = new NavHistoryObserver();
obs[callback.name] = function () {
PlacesUtils.history.removeObserver(this);
callback.apply(this, arguments);
resolve();
};
PlacesUtils.history.addObserver(obs, false);
});
}
/**
@ -54,15 +54,17 @@ add_task(function* test_onVisit() {
let promiseNotify = onNotify(function onVisit(aURI, aVisitID, aTime,
aSessionID, aReferringID,
aTransitionType, aGUID,
aHidden) {
do_check_true(aURI.equals(testuri));
do_check_true(aVisitID > 0);
do_check_eq(aTime, testtime);
do_check_eq(aSessionID, 0);
do_check_eq(aReferringID, 0);
do_check_eq(aTransitionType, TRANSITION_TYPED);
aHidden, aVisitCount, aTyped) {
Assert.ok(aURI.equals(testuri));
Assert.ok(aVisitID > 0);
Assert.equal(aTime, testtime);
Assert.equal(aSessionID, 0);
Assert.equal(aReferringID, 0);
Assert.equal(aTransitionType, TRANSITION_TYPED);
do_check_guid_for_uri(aURI, aGUID);
do_check_false(aHidden);
Assert.ok(!aHidden);
Assert.equal(aVisitCount, 1);
Assert.equal(aTyped, 1);
});
let testuri = NetUtil.newURI("http://firefox.com/");
let testtime = Date.now() * 1000;
@ -74,15 +76,17 @@ add_task(function* test_onVisit() {
let promiseNotify = onNotify(function onVisit(aURI, aVisitID, aTime,
aSessionID, aReferringID,
aTransitionType, aGUID,
aHidden) {
do_check_true(aURI.equals(testuri));
do_check_true(aVisitID > 0);
do_check_eq(aTime, testtime);
do_check_eq(aSessionID, 0);
do_check_eq(aReferringID, 0);
do_check_eq(aTransitionType, TRANSITION_FRAMED_LINK);
aHidden, aVisitCount, aTyped) {
Assert.ok(aURI.equals(testuri));
Assert.ok(aVisitID > 0);
Assert.equal(aTime, testtime);
Assert.equal(aSessionID, 0);
Assert.equal(aReferringID, 0);
Assert.equal(aTransitionType, TRANSITION_FRAMED_LINK);
do_check_guid_for_uri(aURI, aGUID);
do_check_true(aHidden);
Assert.ok(aHidden);
Assert.equal(aVisitCount, 1);
Assert.equal(aTyped, 0);
});
let testuri = NetUtil.newURI("http://hidden.firefox.com/");
let testtime = Date.now() * 1000;
@ -90,12 +94,60 @@ add_task(function* test_onVisit() {
yield promiseNotify;
});
add_task(function* test_multiple_onVisit() {
let testuri = NetUtil.newURI("http://self.firefox.com/");
let promiseNotifications = new Promise(resolve => {
let observer = {
_c: 0,
__proto__: NavHistoryObserver.prototype,
onVisit(uri, id, time, undefined, referrerId, transition, guid,
hidden, visitCount, typed) {
Assert.ok(testuri.equals(uri));
Assert.ok(id > 0);
Assert.ok(time > 0);
Assert.ok(!hidden);
do_check_guid_for_uri(uri, guid);
switch (++this._c) {
case 1:
Assert.equal(referrerId, 0);
Assert.equal(transition, TRANSITION_LINK);
Assert.equal(visitCount, 1);
Assert.equal(typed, 0);
break;
case 2:
Assert.ok(referrerId > 0);
Assert.equal(transition, TRANSITION_LINK);
Assert.equal(visitCount, 2);
Assert.equal(typed, 0);
break;
case 3:
Assert.equal(referrerId, 0);
Assert.equal(transition, TRANSITION_TYPED);
Assert.equal(visitCount, 3);
Assert.equal(typed, 1);
PlacesUtils.history.removeObserver(observer, false);
resolve();
break;
}
}
};
PlacesUtils.history.addObserver(observer, false);
});
yield PlacesTestUtils.addVisits([
{ uri: testuri, transition: TRANSITION_LINK },
{ uri: testuri, referrer: testuri, transition: TRANSITION_LINK },
{ uri: testuri, transition: TRANSITION_TYPED },
]);
yield promiseNotifications;
});
add_task(function* test_onDeleteURI() {
let promiseNotify = onNotify(function onDeleteURI(aURI, aGUID, aReason) {
do_check_true(aURI.equals(testuri));
Assert.ok(aURI.equals(testuri));
// Can't use do_check_guid_for_uri() here because the visit is already gone.
do_check_eq(aGUID, testguid);
do_check_eq(aReason, Ci.nsINavHistoryObserver.REASON_DELETED);
Assert.equal(aGUID, testguid);
Assert.equal(aReason, Ci.nsINavHistoryObserver.REASON_DELETED);
});
let [testuri] = yield task_add_visit();
let testguid = do_get_guid_for_uri(testuri);
@ -106,11 +158,11 @@ add_task(function* test_onDeleteURI() {
add_task(function* test_onDeleteVisits() {
let promiseNotify = onNotify(function onDeleteVisits(aURI, aVisitTime, aGUID,
aReason) {
do_check_true(aURI.equals(testuri));
Assert.ok(aURI.equals(testuri));
// Can't use do_check_guid_for_uri() here because the visit is already gone.
do_check_eq(aGUID, testguid);
do_check_eq(aReason, Ci.nsINavHistoryObserver.REASON_DELETED);
do_check_eq(aVisitTime, 0); // All visits have been removed.
Assert.equal(aGUID, testguid);
Assert.equal(aReason, Ci.nsINavHistoryObserver.REASON_DELETED);
Assert.equal(aVisitTime, 0); // All visits have been removed.
});
let msecs24hrsAgo = Date.now() - (86400 * 1000);
let [testuri] = yield task_add_visit(undefined, msecs24hrsAgo * 1000);
@ -126,8 +178,8 @@ add_task(function* test_onDeleteVisits() {
add_task(function* test_onTitleChanged() {
let promiseNotify = onNotify(function onTitleChanged(aURI, aTitle, aGUID) {
do_check_true(aURI.equals(testuri));
do_check_eq(aTitle, title);
Assert.ok(aURI.equals(testuri));
Assert.equal(aTitle, title);
do_check_guid_for_uri(aURI, aGUID);
});
@ -143,9 +195,9 @@ add_task(function* test_onTitleChanged() {
add_task(function* test_onPageChanged() {
let promiseNotify = onNotify(function onPageChanged(aURI, aChangedAttribute,
aNewValue, aGUID) {
do_check_eq(aChangedAttribute, Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON);
do_check_true(aURI.equals(testuri));
do_check_eq(aNewValue, SMALLPNG_DATA_URI.spec);
Assert.equal(aChangedAttribute, Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON);
Assert.ok(aURI.equals(testuri));
Assert.equal(aNewValue, SMALLPNG_DATA_URI.spec);
do_check_guid_for_uri(aURI, aGUID);
});

View File

@ -0,0 +1,23 @@
# Overview
'safebrowsing.proto' is modified from [1] with the following line added:
"package mozilla.safebrowsing;"
to avoid naming pollution. We use this source file along with protobuf compiler (protoc) to generate safebrowsing.pb.h/cc for safebrowsing v4 update and hash completion. The current generated files are compiled by protoc 2.6.1 since the protobuf library in gecko is not upgraded to 3.0 yet.
# Update
If you want to update to the latest upstream version,
1. Checkout the latest one in [2]
2. Use protoc to generate safebrowsing.pb.h and safebrowsing.pb.cc. For example,
$ protoc -I=. --cpp_out="../protobuf/" safebrowsing.proto
(Note that we should use protoc v2.6.1 [3] to compile. You can find the compiled protoc in [4] if you don't have one.)
[1] https://chromium.googlesource.com/chromium/src.git/+/9c4485f1ce7cac7ae82f7a4ae36ccc663afe806c/components/safe_browsing_db/safebrowsing.proto
[2] https://chromium.googlesource.com/chromium/src.git/+/master/components/safe_browsing_db/safebrowsing.proto
[3] https://github.com/google/protobuf/releases/tag/v2.6.1
[4] https://repo1.maven.org/maven2/com/google/protobuf/protoc

View File

@ -0,0 +1,473 @@
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// This file includes Safe Browsing V4 API blacklist request and response
// protocol buffers. They should be kept in sync with the server implementation.
syntax = "proto2";
option optimize_for = LITE_RUNTIME;
package mozilla.safebrowsing;
message ThreatInfo {
// The threat types to be checked.
repeated ThreatType threat_types = 1;
// The platform types to be checked.
repeated PlatformType platform_types = 2;
// The entry types to be checked.
repeated ThreatEntryType threat_entry_types = 4;
// The threat entries to be checked.
repeated ThreatEntry threat_entries = 3;
}
// A match when checking a threat entry in the Safe Browsing threat lists.
message ThreatMatch {
// The threat type matching this threat.
optional ThreatType threat_type = 1;
// The platform type matching this threat.
optional PlatformType platform_type = 2;
// The threat entry type matching this threat.
optional ThreatEntryType threat_entry_type = 6;
// The threat matching this threat.
optional ThreatEntry threat = 3;
// Optional metadata associated with this threat.
optional ThreatEntryMetadata threat_entry_metadata = 4;
// The cache lifetime for the returned match. Clients must not cache this
// response for more than this duration to avoid false positives.
optional Duration cache_duration = 5;
}
// Request to check entries against lists.
message FindThreatMatchesRequest {
// The client metadata.
optional ClientInfo client = 1;
// The lists and entries to be checked for matches.
optional ThreatInfo threat_info = 2;
}
// Response type for requests to find threat matches.
message FindThreatMatchesResponse {
// The threat list matches.
repeated ThreatMatch matches = 1;
}
// Describes a Safe Browsing API update request. Clients can request updates for
// multiple lists in a single request.
message FetchThreatListUpdatesRequest {
// The client metadata.
optional ClientInfo client = 1;
// A single list update request.
message ListUpdateRequest {
// The type of threat posed by entries present in the list.
optional ThreatType threat_type = 1;
// The type of platform at risk by entries present in the list.
optional PlatformType platform_type = 2;
// The types of entries present in the list.
optional ThreatEntryType threat_entry_type = 5;
// The current state of the client for the requested list (the encrypted
// ClientState that was sent to the client from the previous update
// request).
optional bytes state = 3;
// The constraints for this update.
message Constraints {
// The maximum size in number of entries. The update will not contain more
// entries than this value. This should be a power of 2 between 2**10 and
// 2**20. If zero, no update size limit is set.
optional int32 max_update_entries = 1;
// Sets the maxmimum number of entries that the client is willing to have
// in the local database. This should be a power of 2 between 2**10 and
// 2**20. If zero, no database size limit is set.
optional int32 max_database_entries = 2;
// Requests the list for a specific geographic location. If not set the
// server may pick that value based on the user's IP address. Expects ISO
// 3166-1 alpha-2 format.
optional string region = 3;
// The compression types supported by the client.
repeated CompressionType supported_compressions = 4;
}
// The constraints associated with this request.
optional Constraints constraints = 4;
}
// The requested threat list updates.
repeated ListUpdateRequest list_update_requests = 3;
}
// Response type for threat list update requests.
message FetchThreatListUpdatesResponse {
// An update to an individual list.
message ListUpdateResponse {
// The threat type for which data is returned.
optional ThreatType threat_type = 1;
// The format of the threats.
optional ThreatEntryType threat_entry_type = 2;
// The platform type for which data is returned.
optional PlatformType platform_type = 3;
// The type of response sent to the client.
enum ResponseType {
// Unknown.
RESPONSE_TYPE_UNSPECIFIED = 0;
// Partial updates are applied to the client's existing local database.
PARTIAL_UPDATE = 1;
// Full updates replace the client's entire local database. This means
// that either the client was seriously out-of-date or the client is
// believed to be corrupt.
FULL_UPDATE = 2;
}
// The type of response. This may indicate that an action is required by the
// client when the response is received.
optional ResponseType response_type = 4;
// A set of entries to add to a local threat type's list. Repeated to allow
// for a combination of compressed and raw data to be sent in a single
// response.
repeated ThreatEntrySet additions = 5;
// A set of entries to remove from a local threat type's list. Repeated for
// the same reason as above.
repeated ThreatEntrySet removals = 6;
// The new client state, in encrypted format. Opaque to clients.
optional bytes new_client_state = 7;
// The expected SHA256 hash of the client state; that is, of the sorted list
// of all hashes present in the database after applying the provided update.
// If the client state doesn't match the expected state, the client must
// disregard this update and retry later.
optional Checksum checksum = 8;
}
// The list updates requested by the clients.
repeated ListUpdateResponse list_update_responses = 1;
// The minimum duration the client must wait before issuing any update
// request. If this field is not set clients may update as soon as they want.
optional Duration minimum_wait_duration = 2;
}
// Request to return full hashes matched by the provided hash prefixes.
message FindFullHashesRequest {
// The client metadata.
optional ClientInfo client = 1;
// The current client states for each of the client's local threat lists.
repeated bytes client_states = 2;
// The lists and hashes to be checked.
optional ThreatInfo threat_info = 3;
}
// Response type for requests to find full hashes.
message FindFullHashesResponse {
// The full hashes that matched the requested prefixes.
repeated ThreatMatch matches = 1;
// The minimum duration the client must wait before issuing any find hashes
// request. If this field is not set, clients can issue a request as soon as
// they want.
optional Duration minimum_wait_duration = 2;
// For requested entities that did not match the threat list, how long to
// cache the response.
optional Duration negative_cache_duration = 3;
}
// A hit comprised of multiple resources; one is the threat list entry that was
// encountered by the client, while others give context as to how the client
// arrived at the unsafe entry.
message ThreatHit {
// The threat type reported.
optional ThreatType threat_type = 1;
// The platform type reported.
optional PlatformType platform_type = 2;
// The threat entry responsible for the hit. Full hash should be reported for
// hash-based hits.
optional ThreatEntry entry = 3;
// Types of resources reported by the client as part of a single hit.
enum ThreatSourceType {
// Unknown.
THREAT_SOURCE_TYPE_UNSPECIFIED = 0;
// The URL that matched the threat list (for which GetFullHash returned a
// valid hash).
MATCHING_URL = 1;
// The final top-level URL of the tab that the client was browsing when the
// match occurred.
TAB_URL = 2;
// A redirect URL that was fetched before hitting the final TAB_URL.
TAB_REDIRECT = 3;
}
// A single resource related to a threat hit.
message ThreatSource {
// The URL of the resource.
optional string url = 1;
// The type of source reported.
optional ThreatSourceType type = 2;
// The remote IP of the resource in ASCII format. Either IPv4 or IPv6.
optional string remote_ip = 3;
// Referrer of the resource. Only set if the referrer is available.
optional string referrer = 4;
}
// The resources related to the threat hit.
repeated ThreatSource resources = 4;
}
// Types of threats.
enum ThreatType {
// Unknown.
THREAT_TYPE_UNSPECIFIED = 0;
// Malware threat type.
MALWARE_THREAT = 1;
// Social engineering threat type.
SOCIAL_ENGINEERING_PUBLIC = 2;
// Unwanted software threat type.
UNWANTED_SOFTWARE = 3;
// Potentially harmful application threat type.
POTENTIALLY_HARMFUL_APPLICATION = 4;
// Social engineering threat type for internal use.
SOCIAL_ENGINEERING = 5;
// API abuse threat type.
API_ABUSE = 6;
}
// Types of platforms.
enum PlatformType {
// Unknown platform.
PLATFORM_TYPE_UNSPECIFIED = 0;
// Threat posed to Windows.
WINDOWS_PLATFORM = 1;
// Threat posed to Linux.
LINUX_PLATFORM = 2;
// Threat posed to Android.
// This cannot be ANDROID because that symbol is defined for android builds
// here: build/config/android/BUILD.gn line21.
ANDROID_PLATFORM = 3;
// Threat posed to OSX.
OSX_PLATFORM = 4;
// Threat posed to iOS.
IOS_PLATFORM = 5;
// Threat posed to at least one of the defined platforms.
ANY_PLATFORM = 6;
// Threat posed to all defined platforms.
ALL_PLATFORMS = 7;
// Threat posed to Chrome.
CHROME_PLATFORM = 8;
}
// The client metadata associated with Safe Browsing API requests.
message ClientInfo {
// A client ID that (hopefully) uniquely identifies the client implementation
// of the Safe Browsing API.
optional string client_id = 1;
// The version of the client implementation.
optional string client_version = 2;
}
// The expected state of a client's local database.
message Checksum {
// The SHA256 hash of the client state; that is, of the sorted list of all
// hashes present in the database.
optional bytes sha256 = 1;
}
// The ways in which threat entry sets can be compressed.
enum CompressionType {
// Unknown.
COMPRESSION_TYPE_UNSPECIFIED = 0;
// Raw, uncompressed data.
RAW = 1;
// Rice-Golomb encoded data.
RICE = 2;
}
// An individual threat; for example, a malicious URL or its hash
// representation. Only one of these fields should be set.
message ThreatEntry {
// A variable-length SHA256 hash with size between 4 and 32 bytes inclusive.
optional bytes hash = 1;
// A URL.
optional string url = 2;
}
// Types of entries that pose threats. Threat lists are collections of entries
// of a single type.
enum ThreatEntryType {
// Unspecified.
THREAT_ENTRY_TYPE_UNSPECIFIED = 0;
// A host-suffix/path-prefix URL expression; for example, "foo.bar.com/baz/".
URL = 1;
// An executable program.
EXECUTABLE = 2;
// An IP range.
IP_RANGE = 3;
}
// A set of threats that should be added or removed from a client's local
// database.
message ThreatEntrySet {
// The compression type for the entries in this set.
optional CompressionType compression_type = 1;
// At most one of the following fields should be set.
// The raw SHA256-formatted entries.
optional RawHashes raw_hashes = 2;
// The raw removal indices for a local list.
optional RawIndices raw_indices = 3;
// The encoded 4-byte prefixes of SHA256-formatted entries, using a
// Golomb-Rice encoding.
optional RiceDeltaEncoding rice_hashes = 4;
// The encoded local, lexicographically-sorted list indices, using a
// Golomb-Rice encoding. Used for sending compressed removal indicies.
optional RiceDeltaEncoding rice_indices = 5;
}
// A set of raw indicies to remove from a local list.
message RawIndices {
// The indicies to remove from a lexicographically-sorted local list.
repeated int32 indices = 1;
}
// The uncompressed threat entries in hash format of a particular prefix length.
// Hashes can be anywhere from 4 to 32 bytes in size. A large majority are 4
// bytes, but some hashes are lengthened if they collide with the hash of a
// popular URL.
//
// Used for sending ThreatEntrySet to clients that do not support compression,
// or when sending non-4-byte hashes to clients that do support compression.
message RawHashes {
// The number of bytes for each prefix encoded below. This field can be
// anywhere from 4 (shortest prefix) to 32 (full SHA256 hash).
optional int32 prefix_size = 1;
// The hashes, all concatenated into one long string. Each hash has a prefix
// size of |prefix_size| above. Hashes are sorted in lexicographic order.
optional bytes raw_hashes = 2;
}
// The Rice-Golomb encoded data. Used for sending compressed 4-byte hashes or
// compressed removal indices.
message RiceDeltaEncoding {
// The offset of the first entry in the encoded data, or, if only a single
// integer was encoded, that single integer's value.
optional int64 first_value = 1;
// The Golomb-Rice parameter which is a number between 2 and 28. This field
// is missing (that is, zero) if num_entries is zero.
optional int32 rice_parameter = 2;
// The number of entries that are delta encoded in the encoded data. If only a
// single integer was encoded, this will be zero and the single value will be
// stored in first_value.
optional int32 num_entries = 3;
// The encoded deltas that are encoded using the Golomb-Rice coder.
optional bytes encoded_data = 4;
}
// The metadata associated with a specific threat entry. The client is expected
// to know the metadata key/value pairs associated with each threat type.
message ThreatEntryMetadata {
// A single metadata entry.
message MetadataEntry {
// The metadata entry key.
optional bytes key = 1;
// The metadata entry value.
optional bytes value = 2;
}
// The metadata entries.
repeated MetadataEntry entries = 1;
}
// Describes an individual threat list. A list is defined by three parameters:
// the type of threat posed, the type of platform targeted by the threat, and
// the type of entries in the list.
message ThreatListDescriptor {
// The threat type posed by the list's entries.
optional ThreatType threat_type = 1;
// The platform type targeted by the list's entries.
optional PlatformType platform_type = 2;
// The entry types contained in the list.
optional ThreatEntryType threat_entry_type = 3;
}
// A collection of lists available for download.
message ListThreatListsResponse {
// The lists available for download.
repeated ThreatListDescriptor threat_lists = 1;
}
message Duration {
// Signed seconds of the span of time. Must be from -315,576,000,000
// to +315,576,000,000 inclusive.
optional int64 seconds = 1;
// Signed fractions of a second at nanosecond resolution of the span
// of time. Durations less than one second are represented with a 0
// `seconds` field and a positive or negative `nanos` field. For durations
// of one second or more, a non-zero value for the `nanos` field must be
// of the same sign as the `seconds` field. Must be from -999,999,999
// to +999,999,999 inclusive.
optional int32 nanos = 2;
}

View File

@ -17,6 +17,9 @@ XPIDL_SOURCES += [
XPIDL_MODULE = 'url-classifier'
# Disable RTTI in google protocol buffer
DEFINES['GOOGLE_PROTOBUF_NO_RTTI'] = True
UNIFIED_SOURCES += [
'ChunkSet.cpp',
'Classifier.cpp',
@ -25,6 +28,7 @@ UNIFIED_SOURCES += [
'nsUrlClassifierDBService.cpp',
'nsUrlClassifierProxies.cpp',
'nsUrlClassifierUtils.cpp',
'protobuf/safebrowsing.pb.cc',
'ProtocolParser.cpp',
]
@ -58,6 +62,7 @@ EXPORTS += [
'Entries.h',
'LookupCache.h',
'nsUrlClassifierPrefixSet.h',
'protobuf/safebrowsing.pb.h',
]
FINAL_LIBRARY = 'xul'

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
#include "safebrowsing.pb.h"
#include "gtest/gtest.h"
TEST(SafeBrowsingProtobuf, Empty)
{
using namespace mozilla::safebrowsing;
const std::string CLIENT_ID = "firefox";
// Construct a simple update request.
FetchThreatListUpdatesRequest r;
r.set_allocated_client(new ClientInfo());
r.mutable_client()->set_client_id(CLIENT_ID);
// Then serialize.
std::string s;
r.SerializeToString(&s);
// De-serialize.
FetchThreatListUpdatesRequest r2;
r2.ParseFromString(s);
ASSERT_EQ(r2.client().client_id(), CLIENT_ID);
}

View File

@ -10,6 +10,7 @@ LOCAL_INCLUDES += [
UNIFIED_SOURCES += [
'TestChunkSet.cpp',
'TestSafeBrowsingProtobuf.cpp',
'TestUrlClassifierUtils.cpp',
]

View File

@ -244,6 +244,13 @@ const SIGNED_TYPES = new Set([
"experiment",
]);
// This is a random number array that can be used as "salt" when generating
// an automatic ID based on the directory path of an add-on. It will prevent
// someone from creating an ID for a permanent add-on that could be replaced
// by a temporary add-on (because that would be confusing, I guess).
const TEMP_INSTALL_ID_GEN_SESSION =
new Uint8Array(Float64Array.of(Math.random()).buffer);
// Whether add-on signing is required.
function mustSign(aType) {
if (!SIGNED_TYPES.has(aType))
@ -1329,11 +1336,19 @@ var loadManifestFromDir = Task.async(function*(aDir, aInstallLocation) {
addon = yield loadManifestFromWebManifest(uri);
if (!addon.id) {
if (aInstallLocation == TemporaryInstallLocation) {
let id = Cc["@mozilla.org/uuid-generator;1"]
.getService(Ci.nsIUUIDGenerator)
.generateUUID().toString();
logger.info(`Generated temporary id ${id} for ${aDir.path}`);
addon.id = id;
// Generate a unique ID based on the directory path of
// this temporary add-on location.
const hasher = Cc["@mozilla.org/security/hash;1"]
.createInstance(Ci.nsICryptoHash);
hasher.init(hasher.SHA1);
const data = new TextEncoder().encode(aDir.path);
// Make it so this ID cannot be guessed.
const sess = TEMP_INSTALL_ID_GEN_SESSION;
hasher.update(sess, sess.length);
hasher.update(data, data.length);
addon.id = `${getHashStringForCrypto(hasher)}@temporary-addon`;
logger.info(
`Generated temp id ${addon.id} (${sess.join("")}) for ${aDir.path}`);
} else {
addon.id = aDir.leafName;
}

View File

@ -46,6 +46,71 @@ add_task(function* test_implicit_id_temp() {
addon.uninstall();
});
// We should be able to temporarily install an unsigned web extension
// that does not have an ID in its manifest.
add_task(function* test_unsigned_no_id_temp_install() {
if (!TEST_UNPACKED) {
do_print("This test does not apply when using packed extensions");
return;
}
const manifest = {
name: "no ID",
description: "extension without an ID",
manifest_version: 2,
version: "1.0"
};
const addonDir = writeWebManifestForExtension(manifest, gTmpD,
"the-addon-sub-dir");
const addon = yield AddonManager.installTemporaryAddon(addonDir);
ok(addon.id, "ID should have been auto-generated");
// Install the same directory again, as if re-installing or reloading.
const secondAddon = yield AddonManager.installTemporaryAddon(addonDir);
// The IDs should be the same.
equal(secondAddon.id, addon.id);
secondAddon.uninstall();
addonDir.remove(true);
});
// We should be able to install two extensions from manifests without IDs
// at different locations and get two unique extensions.
add_task(function* test_multiple_no_id_extensions() {
if (!TEST_UNPACKED) {
do_print("This test does not apply when using packed extensions");
return;
}
const manifest = {
name: "no ID",
description: "extension without an ID",
manifest_version: 2,
version: "1.0"
};
const firstAddonDir = writeWebManifestForExtension(manifest, gTmpD,
"addon-sub-dir-one");
const secondAddonDir = writeWebManifestForExtension(manifest, gTmpD,
"addon-sub-dir-two");
const [firstAddon, secondAddon] = yield Promise.all([
AddonManager.installTemporaryAddon(firstAddonDir),
AddonManager.installTemporaryAddon(secondAddonDir)
]);
const allAddons = yield new Promise(resolve => {
AddonManager.getAllAddons(addons => resolve(addons));
});
do_print(`Found these add-ons: ${allAddons.map(a => a.name).join(", ")}`);
const filtered = allAddons.filter(addon => addon.name === manifest.name);
// Make sure we have two add-ons by the same name.
equal(filtered.length, 2);
firstAddon.uninstall();
firstAddonDir.remove(true);
secondAddon.uninstall();
secondAddonDir.remove(true);
});
// Test that we can get the ID from browser_specific_settings
add_task(function* test_bss_id() {
const ID = "webext_bss_id@tests.mozilla.org";