forked from mirrors/gecko-dev
396 lines
11 KiB
JavaScript
396 lines
11 KiB
JavaScript
/* 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";
|
|
|
|
var EXPORTED_SYMBOLS = ["Interactions"];
|
|
|
|
const { XPCOMUtils } = ChromeUtils.import(
|
|
"resource://gre/modules/XPCOMUtils.jsm"
|
|
);
|
|
|
|
XPCOMUtils.defineLazyModuleGetters(this, {
|
|
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
|
|
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
|
|
Services: "resource://gre/modules/Services.jsm",
|
|
});
|
|
|
|
XPCOMUtils.defineLazyServiceGetters(this, {
|
|
idleService: ["@mozilla.org/widget/useridleservice;1", "nsIUserIdleService"],
|
|
});
|
|
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
this,
|
|
"pageViewIdleTime",
|
|
"browser.places.interactions.pageViewIdleTime",
|
|
60
|
|
);
|
|
|
|
const DOMWINDOW_OPENED_TOPIC = "domwindowopened";
|
|
|
|
/**
|
|
* @typedef {object} DocumentInfo
|
|
* DocumentInfo is used to pass document information from the child process
|
|
* to _Interactions.
|
|
* @property {boolean} isActive
|
|
* Set to true if the document is active, i.e. visible.
|
|
* @property {string} url
|
|
* The url of the page that was interacted with.
|
|
*/
|
|
|
|
/**
|
|
* @typedef {object} InteractionInfo
|
|
* InteractionInfo is used to store information associated with interactions.
|
|
* @property {number} totalViewTime
|
|
* Time in milliseconds that the page has been actively viewed for.
|
|
* @property {string} url
|
|
* The url of the page that was interacted with.
|
|
*/
|
|
|
|
/**
|
|
* The Interactions object sets up listeners and other approriate tools for
|
|
* obtaining interaction information and passing it to the InteractionsManager.
|
|
*/
|
|
class _Interactions {
|
|
/**
|
|
* This is used to store potential interactions. It maps the browser
|
|
* to the current interaction information.
|
|
* The current interaction is updated to the database when it transitions
|
|
* to non-active, which occurs before a browser tab is closed, hence this
|
|
* can be a weak map.
|
|
*
|
|
* @type {WeakMap<browser, InteractionInfo>}
|
|
*/
|
|
#interactions = new WeakMap();
|
|
|
|
/**
|
|
* Tracks the currently active window so that we can avoid recording
|
|
* interactions in non-active windows.
|
|
*
|
|
* @type {DOMWindow}
|
|
*/
|
|
#activeWindow = undefined;
|
|
|
|
/**
|
|
* Tracks if the user is idle.
|
|
*
|
|
* @type {boolean}
|
|
*/
|
|
#userIsIdle = false;
|
|
|
|
/**
|
|
* This stores the page view start time of the current page view.
|
|
* For any single page view, this may be moved multiple times as the
|
|
* associated interaction is updated for the current total page view time.
|
|
*
|
|
* @type {number}
|
|
*/
|
|
_pageViewStartTime = Cu.now();
|
|
|
|
/**
|
|
* Initializes, sets up actors and observers.
|
|
*/
|
|
init() {
|
|
if (
|
|
!Services.prefs.getBoolPref("browser.places.interactions.enabled", false)
|
|
) {
|
|
return;
|
|
}
|
|
|
|
this.logConsole = console.createInstance({
|
|
prefix: "InteractionsManager",
|
|
maxLogLevel: Services.prefs.getBoolPref(
|
|
"browser.places.interactions.log",
|
|
false
|
|
)
|
|
? "Debug"
|
|
: "Warn",
|
|
});
|
|
this.logConsole.debug("init");
|
|
|
|
ChromeUtils.registerWindowActor("Interactions", {
|
|
parent: {
|
|
moduleURI: "resource:///actors/InteractionsParent.jsm",
|
|
},
|
|
child: {
|
|
moduleURI: "resource:///actors/InteractionsChild.jsm",
|
|
group: "browsers",
|
|
events: {
|
|
DOMContentLoaded: {},
|
|
pagehide: { mozSystemGroup: true },
|
|
},
|
|
},
|
|
});
|
|
|
|
this.#activeWindow = Services.wm.getMostRecentBrowserWindow();
|
|
|
|
for (let win of BrowserWindowTracker.orderedWindows) {
|
|
if (!win.closed) {
|
|
this.#registerWindow(win);
|
|
}
|
|
}
|
|
Services.obs.addObserver(this, DOMWINDOW_OPENED_TOPIC, true);
|
|
idleService.addIdleObserver(this, pageViewIdleTime);
|
|
}
|
|
|
|
/**
|
|
* Uninitializes, removes any observers that need cleaning up manually.
|
|
*/
|
|
uninit() {
|
|
idleService.removeIdleObserver(this, pageViewIdleTime);
|
|
}
|
|
|
|
/**
|
|
* Registers the start of a new interaction.
|
|
*
|
|
* @param {Browser} browser
|
|
* The browser object associated with the interaction.
|
|
* @param {DocumentInfo} docInfo
|
|
* The document information of the page associated with the interaction.
|
|
*/
|
|
registerNewInteraction(browser, docInfo) {
|
|
let interaction = this.#interactions.get(browser);
|
|
if (interaction && interaction.url != docInfo.url) {
|
|
this.registerEndOfInteraction(browser);
|
|
}
|
|
|
|
this.logConsole.debug("New interaction", docInfo);
|
|
interaction = {
|
|
url: docInfo.url,
|
|
totalViewTime: 0,
|
|
};
|
|
this.#interactions.set(browser, interaction);
|
|
|
|
// Only reset the time if this is being loaded in the active tab of the
|
|
// active window.
|
|
if (docInfo.isActive && browser.ownerGlobal == this.#activeWindow) {
|
|
this._pageViewStartTime = Cu.now();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Registers the end of an interaction, e.g. if the user navigates away
|
|
* from the page. This will store the final interaction details and clear
|
|
* the current interaction.
|
|
*
|
|
* @param {Browser} browser
|
|
* The browser object associated with the interaction.
|
|
*/
|
|
registerEndOfInteraction(browser) {
|
|
// Not having a browser passed to us probably means the tab has gone away
|
|
// before we received the notification - due to the tab being a background
|
|
// tab. Since that will be a non-active tab, it is acceptable that we don't
|
|
// update the interaction. When switching away from active tabs, a TabSelect
|
|
// notification is generated which we handle elsewhere.
|
|
if (!browser) {
|
|
return;
|
|
}
|
|
this.logConsole.debug("End of interaction");
|
|
|
|
this.#updateInteraction(browser);
|
|
this.#interactions.delete(browser);
|
|
}
|
|
|
|
/**
|
|
* Updates the current interaction.
|
|
*
|
|
* @param {Browser} [browser]
|
|
* The browser object that has triggered the update, if known. This is
|
|
* used to check if the browser is in the active window, and as an
|
|
* optimization to avoid obtaining the browser object.
|
|
*/
|
|
#updateInteraction(browser = undefined) {
|
|
if (
|
|
!this.#activeWindow ||
|
|
(browser && browser.ownerGlobal != this.#activeWindow)
|
|
) {
|
|
this.logConsole.debug("No update due to no active window");
|
|
return;
|
|
}
|
|
|
|
// We do not update the interaction when the user is idle, since we will
|
|
// have already updated it when idle was signalled.
|
|
// Sometimes an interaction may be signalled before idle is cleared, however
|
|
// worst case we'd only loose approx 2 seconds of interaction detail.
|
|
if (this.#userIsIdle) {
|
|
this.logConsole.debug("No update due to user is idle");
|
|
return;
|
|
}
|
|
|
|
if (!browser) {
|
|
browser = this.#activeWindow.gBrowser.selectedTab.linkedBrowser;
|
|
}
|
|
|
|
let interaction = this.#interactions.get(browser);
|
|
if (!interaction) {
|
|
this.logConsole.debug("No interaction to update");
|
|
return;
|
|
}
|
|
|
|
interaction.totalViewTime += Cu.now() - this._pageViewStartTime;
|
|
this._pageViewStartTime = Cu.now();
|
|
this._updateDatabase(interaction);
|
|
}
|
|
|
|
/**
|
|
* Handles a window becoming active.
|
|
*
|
|
* @param {DOMWindow} win
|
|
*/
|
|
#onActivateWindow(win) {
|
|
this.logConsole.debug("Activate window");
|
|
|
|
if (PrivateBrowsingUtils.isWindowPrivate(win)) {
|
|
return;
|
|
}
|
|
|
|
this.#activeWindow = win;
|
|
this._pageViewStartTime = Cu.now();
|
|
}
|
|
|
|
/**
|
|
* Handles a window going inactive.
|
|
*
|
|
* @param {DOMWindow} win
|
|
*/
|
|
#onDeactivateWindow(win) {
|
|
this.logConsole.debug("Deactivate window");
|
|
|
|
this.#updateInteraction();
|
|
this.#activeWindow = undefined;
|
|
}
|
|
|
|
/**
|
|
* Handles the TabSelect notification. Updates the current interaction and
|
|
* then switches it to the interaction for the new tab. The new interaction
|
|
* may be null if it doesn't exist.
|
|
*
|
|
* @param {Browser} previousBrowser
|
|
* The instance of the browser that the user switched away from.
|
|
*/
|
|
#onTabSelect(previousBrowser) {
|
|
this.logConsole.debug("Tab switch notified");
|
|
|
|
this.#updateInteraction(previousBrowser);
|
|
this._pageViewStartTime = Cu.now();
|
|
}
|
|
|
|
/**
|
|
* Handles various events and forwards them to appropriate functions.
|
|
*
|
|
* @param {DOMEvent} event
|
|
*/
|
|
handleEvent(event) {
|
|
switch (event.type) {
|
|
case "TabSelect":
|
|
this.#onTabSelect(event.detail.previousTab.linkedBrowser);
|
|
break;
|
|
case "activate":
|
|
this.#onActivateWindow(event.target);
|
|
break;
|
|
case "deactivate":
|
|
this.#onDeactivateWindow(event.target);
|
|
break;
|
|
case "unload":
|
|
this.#unregisterWindow(event.target);
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles notifications from the observer service.
|
|
*
|
|
* @param {nsISupports} subject
|
|
* @param {string} topic
|
|
* @param {string} data
|
|
*/
|
|
observe(subject, topic, data) {
|
|
switch (topic) {
|
|
case DOMWINDOW_OPENED_TOPIC:
|
|
this.#onWindowOpen(subject);
|
|
break;
|
|
case "idle":
|
|
this.logConsole.debug("idle");
|
|
// We save the state of the current interaction when we are notified
|
|
// that the user is idle.
|
|
this.#updateInteraction();
|
|
this.#userIsIdle = true;
|
|
break;
|
|
case "active":
|
|
this.logConsole.debug("active");
|
|
this.#userIsIdle = false;
|
|
this._pageViewStartTime = Cu.now();
|
|
break;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles registration of listeners in a new window.
|
|
*
|
|
* @param {DOMWindow} win
|
|
* The window to register in.
|
|
*/
|
|
#registerWindow(win) {
|
|
if (PrivateBrowsingUtils.isWindowPrivate(win)) {
|
|
return;
|
|
}
|
|
|
|
win.addEventListener("TabSelect", this, true);
|
|
win.addEventListener("deactivate", this, true);
|
|
win.addEventListener("activate", this, true);
|
|
}
|
|
|
|
/**
|
|
* Handles removing of listeners from a window.
|
|
*
|
|
* @param {DOMWindow} win
|
|
* The window to remove listeners from.
|
|
*/
|
|
#unregisterWindow(win) {
|
|
win.removeEventListener("TabSelect", this, true);
|
|
win.removeEventListener("deactivate", this, true);
|
|
win.removeEventListener("activate", this, true);
|
|
}
|
|
|
|
/**
|
|
* Handles a new window being opened, waits for load and checks that
|
|
* it is a browser window, then adds listeners.
|
|
*
|
|
* @param {DOMWindow} win
|
|
* The window being opened.
|
|
*/
|
|
#onWindowOpen(win) {
|
|
win.addEventListener(
|
|
"load",
|
|
() => {
|
|
if (
|
|
win.document.documentElement.getAttribute("windowtype") !=
|
|
"navigator:browser"
|
|
) {
|
|
return;
|
|
}
|
|
this.#registerWindow(win);
|
|
},
|
|
{ once: true }
|
|
);
|
|
}
|
|
|
|
QueryInterface = ChromeUtils.generateQI([
|
|
"nsIObserver",
|
|
"nsISupportsWeakReference",
|
|
]);
|
|
|
|
/**
|
|
* Temporary test-only function to allow testing what we write to the
|
|
* database is correct, whilst we haven't got a database.
|
|
*
|
|
* @param {InteractionInfo} interactionInfo
|
|
* The document information to write.
|
|
*/
|
|
async _updateDatabase(interactionInfo) {
|
|
this.logConsole.debug("Would update database: ", interactionInfo);
|
|
}
|
|
}
|
|
|
|
const Interactions = new _Interactions();
|