mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-24 21:31:04 +00:00
Merge fx-team to central, a=merge
This commit is contained in:
commit
07d01350a8
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
},
|
||||
|
||||
|
@ -155,7 +155,6 @@
|
||||
},
|
||||
{
|
||||
"name": "getVisits",
|
||||
"unsupported": true,
|
||||
"type": "function",
|
||||
"description": "Retrieves information about visits to a URL.",
|
||||
"async": "callback",
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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]
|
||||
|
@ -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],
|
||||
|
@ -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],
|
||||
|
@ -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;
|
||||
}
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ add_task(function* () {
|
||||
expectUncaughtException();
|
||||
}
|
||||
|
||||
content.location = TEST_URI;
|
||||
BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI);
|
||||
|
||||
yield waitForMessages({
|
||||
webconsole: hud,
|
||||
|
@ -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();
|
||||
|
@ -179,7 +179,7 @@ function testNext() {
|
||||
startNextTest();
|
||||
}, true);
|
||||
|
||||
content.location = testLocation;
|
||||
BrowserTestUtils.loadURI(gBrowser.selectedBrowser, testLocation);
|
||||
} else {
|
||||
testEnded = true;
|
||||
finishTest();
|
||||
|
@ -37,7 +37,7 @@ function consoleOpened(hud) {
|
||||
deferred.resolve();
|
||||
}
|
||||
};
|
||||
content.location = TEST_URI2;
|
||||
BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI2);
|
||||
|
||||
return deferred.promise;
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ add_task(function* () {
|
||||
expectUncaughtException();
|
||||
}
|
||||
|
||||
content.location = TEST_URI;
|
||||
BrowserTestUtils.loadURI(gBrowser.selectedBrowser, TEST_URI);
|
||||
yield loaded;
|
||||
|
||||
yield testWebDevLimits();
|
||||
|
@ -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");
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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({
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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");
|
||||
};
|
||||
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
@ -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* () {
|
||||
|
@ -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* () {
|
||||
|
@ -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* () {
|
||||
|
@ -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");
|
||||
|
@ -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");
|
||||
|
@ -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");
|
||||
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
||||
});
|
||||
|
@ -24,6 +24,7 @@ DevToolsModules(
|
||||
'string.js',
|
||||
'styles.js',
|
||||
'stylesheets.js',
|
||||
'timeline.js',
|
||||
'webaudio.js',
|
||||
'webgl.js'
|
||||
)
|
||||
|
25
devtools/shared/fronts/timeline.js
Normal file
25
devtools/shared/fronts/timeline.js
Normal 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;
|
@ -32,6 +32,7 @@ DevToolsModules(
|
||||
'styleeditor.js',
|
||||
'styles.js',
|
||||
'stylesheets.js',
|
||||
'timeline.js',
|
||||
'webaudio.js',
|
||||
'webgl.js',
|
||||
'worker.js'
|
||||
|
118
devtools/shared/specs/timeline.js
Normal file
118
devtools/shared/specs/timeline.js
Normal 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;
|
@ -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()});`;
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
423
mobile/android/base/java/org/mozilla/gecko/home/RecentTabsAdapter.java
Executable file
423
mobile/android/base/java/org/mozilla/gecko/home/RecentTabsAdapter.java
Executable 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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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. -->
|
||||
|
@ -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',
|
||||
|
BIN
mobile/android/base/resources/drawable-nodpi/icon_recent.png
Executable file
BIN
mobile/android/base/resources/drawable-nodpi/icon_recent.png
Executable file
Binary file not shown.
After Width: | Height: | Size: 774 B |
@ -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>
|
@ -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>
|
@ -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" />
|
||||
|
@ -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>
|
@ -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>
|
@ -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"/>
|
@ -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>
|
||||
|
@ -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);
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
@ -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",
|
||||
|
@ -2,4 +2,5 @@
|
||||
support-files =
|
||||
head.js
|
||||
|
||||
[test_ext_pageAction.html]
|
||||
[test_ext_pageAction.html]
|
||||
[test_ext_pageAction_popup.html]
|
@ -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>
|
@ -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()
|
||||
{
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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");
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
//
|
||||
|
@ -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" \
|
||||
)
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
});
|
@ -6,3 +6,4 @@ tail =
|
||||
[test_remove.js]
|
||||
[test_removeVisits.js]
|
||||
[test_removeVisitsByFilter.js]
|
||||
[test_updatePlaces_sameUri_titleChanged.js]
|
||||
|
@ -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);
|
||||
});
|
||||
|
||||
|
23
toolkit/components/url-classifier/chromium/README.txt
Normal file
23
toolkit/components/url-classifier/chromium/README.txt
Normal 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
|
473
toolkit/components/url-classifier/chromium/safebrowsing.proto
Normal file
473
toolkit/components/url-classifier/chromium/safebrowsing.proto
Normal 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;
|
||||
}
|
@ -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'
|
||||
|
7166
toolkit/components/url-classifier/protobuf/safebrowsing.pb.cc
Normal file
7166
toolkit/components/url-classifier/protobuf/safebrowsing.pb.cc
Normal file
File diff suppressed because it is too large
Load Diff
6283
toolkit/components/url-classifier/protobuf/safebrowsing.pb.h
Normal file
6283
toolkit/components/url-classifier/protobuf/safebrowsing.pb.h
Normal file
File diff suppressed because it is too large
Load Diff
@ -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);
|
||||
}
|
@ -10,6 +10,7 @@ LOCAL_INCLUDES += [
|
||||
|
||||
UNIFIED_SOURCES += [
|
||||
'TestChunkSet.cpp',
|
||||
'TestSafeBrowsingProtobuf.cpp',
|
||||
'TestUrlClassifierUtils.cpp',
|
||||
]
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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";
|
||||
|
Loading…
Reference in New Issue
Block a user