mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1225690 - implement a SyncedTabs module to abstract away Sync's internals. r=rnewman
This commit is contained in:
parent
ca321b8b4e
commit
6870575107
267
services/sync/modules/SyncedTabs.jsm
Normal file
267
services/sync/modules/SyncedTabs.jsm
Normal file
@ -0,0 +1,267 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["SyncedTabs"];
|
||||
|
||||
|
||||
const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Task.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
Cu.import("resource://gre/modules/PlacesUtils.jsm", this);
|
||||
Cu.import("resource://services-sync/main.js");
|
||||
Cu.import("resource://gre/modules/Preferences.jsm");
|
||||
|
||||
// The Sync XPCOM service
|
||||
XPCOMUtils.defineLazyGetter(this, "weaveXPCService", function() {
|
||||
return Cc["@mozilla.org/weave/service;1"]
|
||||
.getService(Ci.nsISupports)
|
||||
.wrappedJSObject;
|
||||
});
|
||||
|
||||
// from MDN...
|
||||
function escapeRegExp(string) {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
// A topic we fire whenever we have new tabs available. This might be due
|
||||
// to a request made by this module to refresh the tab list, or as the result
|
||||
// of a regularly scheduled sync. The intent is that consumers just listen
|
||||
// for this notification and update their UI in response.
|
||||
const TOPIC_TABS_CHANGED = "services.sync.tabs.changed";
|
||||
|
||||
// The interval, in seconds, before which we consider the existing list
|
||||
// of tabs "fresh enough" and don't force a new sync.
|
||||
const TABS_FRESH_ENOUGH_INTERVAL = 30;
|
||||
|
||||
let log = Log.repository.getLogger("Sync.RemoteTabs");
|
||||
// A new scope to do the logging thang...
|
||||
(function() {
|
||||
let level = Preferences.get("services.sync.log.logger.tabs");
|
||||
if (level) {
|
||||
let appender = new Log.DumpAppender();
|
||||
log.level = appender.level = Log.Level[level] || Log.Level.Debug;
|
||||
log.addAppender(appender);
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
// A private singleton that does the work.
|
||||
let SyncedTabsInternal = {
|
||||
_getClientIcon(id) {
|
||||
let isMobile = Weave.Service.clientsEngine.isMobile(id);
|
||||
if (isMobile) {
|
||||
return "chrome://browser/skin/sync-mobileIcon.png";
|
||||
}
|
||||
return "chrome://browser/skin/sync-desktopIcon.png";
|
||||
},
|
||||
|
||||
/* Make a "tab" record. Returns a promise */
|
||||
_makeTab: Task.async(function* (client, tab, url) {
|
||||
let icon = tab.icon;
|
||||
if (!icon) {
|
||||
try {
|
||||
icon = (yield PlacesUtils.promiseFaviconLinkUrl(url)).spec;
|
||||
} catch (ex) { /* no favicon avaiable */ }
|
||||
}
|
||||
if (!icon) {
|
||||
icon = PlacesUtils.favicons.defaultFavicon.spec;
|
||||
}
|
||||
return {
|
||||
type: "tab",
|
||||
title: tab.title || url,
|
||||
url,
|
||||
icon,
|
||||
client: client.id,
|
||||
lastUsed: tab.lastUsed,
|
||||
};
|
||||
}),
|
||||
|
||||
/* Make a "client" record. Returns a promise for consistency with _makeTab */
|
||||
_makeClient: Task.async(function* (client) {
|
||||
return {
|
||||
id: client.id,
|
||||
type: "client",
|
||||
name: client.clientName,
|
||||
icon: this._getClientIcon(client.id),
|
||||
tabs: []
|
||||
};
|
||||
}),
|
||||
|
||||
_tabMatchesFilter(tab, filter) {
|
||||
let reFilter = new RegExp(escapeRegExp(filter), "i");
|
||||
return tab.url.match(reFilter) || tab.title.match(reFilter);
|
||||
},
|
||||
|
||||
getTabClients: Task.async(function* (filter) {
|
||||
log.info("Generating tab list with filter", filter);
|
||||
let result = [];
|
||||
|
||||
// If Sync isn't ready, don't try and get anything.
|
||||
if (!weaveXPCService.ready) {
|
||||
log.debug("Sync isn't yet ready, so returning an empty tab list");
|
||||
return result;
|
||||
}
|
||||
|
||||
let engine = Weave.Service.engineManager.get("tabs");
|
||||
|
||||
let seenURLs = new Set();
|
||||
let parentIndex = 0;
|
||||
let ntabs = 0;
|
||||
|
||||
for (let [guid, client] in Iterator(engine.getAllClients())) {
|
||||
let clientRepr = yield this._makeClient(client);
|
||||
log.debug("Processing client", clientRepr);
|
||||
|
||||
for (let tab of client.tabs) {
|
||||
let url = tab.urlHistory[0];
|
||||
log.debug("remote tab", url);
|
||||
// Note there are some issues with tracking "seen" tabs, including:
|
||||
// * We really can't return the entire urlHistory record as we are
|
||||
// only checking the first entry - others might be different.
|
||||
// * We don't update the |lastUsed| timestamp to reflect the
|
||||
// most-recently-seen time.
|
||||
// In a followup we should consider simply dropping this |seenUrls|
|
||||
// check and return duplicate records - it seems the user will be more
|
||||
// confused by tabs not showing up on a device (because it was detected
|
||||
// as a dupe so it only appears on a different device) than being
|
||||
// confused by seeing the same tab on different clients.
|
||||
if (!url || seenURLs.has(url)) {
|
||||
continue;
|
||||
}
|
||||
let tabRepr = yield this._makeTab(client, tab, url);
|
||||
if (filter && !this._tabMatchesFilter(tabRepr, filter)) {
|
||||
continue;
|
||||
}
|
||||
seenURLs.add(url);
|
||||
clientRepr.tabs.push(tabRepr);
|
||||
}
|
||||
// We return all clients, even those without tabs - the consumer should
|
||||
// filter it if they care.
|
||||
ntabs += clientRepr.tabs.length;
|
||||
result.push(clientRepr);
|
||||
}
|
||||
log.info(`Final tab list has ${result.length} clients with ${ntabs} tabs.`);
|
||||
return result;
|
||||
}),
|
||||
|
||||
syncTabs(force) {
|
||||
if (!force) {
|
||||
// Don't bother refetching tabs if we already did so recently
|
||||
let lastFetch = Preferences.get("services.sync.lastTabFetch", 0);
|
||||
let now = Math.floor(Date.now() / 1000);
|
||||
if (now - lastFetch < TABS_FRESH_ENOUGH_INTERVAL) {
|
||||
log.info("_refetchTabs was done recently, do not doing it again");
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
}
|
||||
|
||||
// If Sync isn't configured don't try and sync, else we will get reports
|
||||
// of a login failure.
|
||||
if (Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED) {
|
||||
log.info("Sync client is not configured, so not attempting a tab sync");
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
// Ask Sync to just do the tabs engine if it can.
|
||||
// Sync is currently synchronous, so do it after an event-loop spin to help
|
||||
// keep the UI responsive.
|
||||
return new Promise((resolve, reject) => {
|
||||
Services.tm.currentThread.dispatch(() => {
|
||||
try {
|
||||
log.info("Doing a tab sync.");
|
||||
Weave.Service.sync(["tabs"]);
|
||||
resolve(true);
|
||||
} catch (ex) {
|
||||
log.error("Sync failed", ex);
|
||||
reject(ex);
|
||||
};
|
||||
}, Ci.nsIThread.DISPATCH_NORMAL);
|
||||
});
|
||||
},
|
||||
|
||||
observe(subject, topic, data) {
|
||||
log.trace(`observed topic=${topic}, data=${data}, subject=${subject}`);
|
||||
switch (topic) {
|
||||
case "weave:engine:sync:finish":
|
||||
if (data != "tabs") {
|
||||
return;
|
||||
}
|
||||
// The tabs engine just finished syncing
|
||||
// Set our lastTabFetch pref here so it tracks both explicit sync calls
|
||||
// and normally scheduled ones.
|
||||
Preferences.set("services.sync.lastTabFetch", Math.floor(Date.now() / 1000));
|
||||
Services.obs.notifyObservers(null, TOPIC_TABS_CHANGED, null);
|
||||
break;
|
||||
case "weave:service:start-over":
|
||||
// start-over needs to notify so consumers find no tabs.
|
||||
Preferences.reset("services.sync.lastTabFetch");
|
||||
Services.obs.notifyObservers(null, TOPIC_TABS_CHANGED, null);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
// Returns true if Sync is configured to Sync tabs, false otherwise
|
||||
get isConfiguredToSyncTabs() {
|
||||
if (!weaveXPCService.ready) {
|
||||
log.debug("Sync isn't yet ready; assuming tab engine is enabled");
|
||||
return true;
|
||||
}
|
||||
|
||||
let engine = Weave.Service.engineManager.get("tabs");
|
||||
return engine && engine.enabled;
|
||||
},
|
||||
|
||||
get hasSyncedThisSession() {
|
||||
let engine = Weave.Service.engineManager.get("tabs");
|
||||
return engine && engine.hasSyncedThisSession;
|
||||
},
|
||||
};
|
||||
|
||||
Services.obs.addObserver(SyncedTabsInternal, "weave:engine:sync:finish", false);
|
||||
Services.obs.addObserver(SyncedTabsInternal, "weave:service:start-over", false);
|
||||
|
||||
// The public interface.
|
||||
this.SyncedTabs = {
|
||||
// A mock-point for tests.
|
||||
_internal: SyncedTabsInternal,
|
||||
|
||||
// We make the topic for the observer notification public.
|
||||
TOPIC_TABS_CHANGED,
|
||||
|
||||
// Returns true if Sync is configured to Sync tabs, false otherwise
|
||||
get isConfiguredToSyncTabs() {
|
||||
return this._internal.isConfiguredToSyncTabs;
|
||||
},
|
||||
|
||||
// Returns true if a tab sync has completed once this session. If this
|
||||
// returns false, then getting back no clients/tabs possibly just means we
|
||||
// are waiting for that first sync to complete.
|
||||
get hasSyncedThisSession() {
|
||||
return this._internal.hasSyncedThisSession;
|
||||
},
|
||||
|
||||
// Return a promise that resolves with an array of client records, each with
|
||||
// a .tabs array. Note that part of the contract for this module is that the
|
||||
// returned objects are not shared between invocations, so callers are free
|
||||
// to mutate the returned objects (eg, sort, truncate) however they see fit.
|
||||
getTabClients(query) {
|
||||
return this._internal.getTabClients(query);
|
||||
},
|
||||
|
||||
// Starts a background request to start syncing tabs. Returns a promise that
|
||||
// resolves when the sync is complete, but there's no resolved value -
|
||||
// callers should be listening for TOPIC_TABS_CHANGED.
|
||||
// If |force| is true we always sync. If false, we only sync if the most
|
||||
// recent sync wasn't "recently".
|
||||
syncTabs(force) {
|
||||
return this._internal.syncTabs(force);
|
||||
},
|
||||
};
|
||||
|
@ -43,6 +43,11 @@ TabEngine.prototype = {
|
||||
_storeObj: TabStore,
|
||||
_trackerObj: TabTracker,
|
||||
_recordObj: TabSetRecord,
|
||||
// A flag to indicate if we have synced in this session. This is to help
|
||||
// consumers of remote tabs that may want to differentiate between "I've an
|
||||
// empty tab list as I haven't yet synced" vs "I've an empty tab list
|
||||
// as there really are no tabs"
|
||||
hasSyncedThisSession: false,
|
||||
|
||||
syncPriority: 3,
|
||||
|
||||
@ -67,6 +72,7 @@ TabEngine.prototype = {
|
||||
SyncEngine.prototype._resetClient.call(this);
|
||||
this._store.wipe();
|
||||
this._tracker.modified = true;
|
||||
this.hasSyncedThisSession = false;
|
||||
},
|
||||
|
||||
removeClientData: function () {
|
||||
@ -94,7 +100,12 @@ TabEngine.prototype = {
|
||||
}
|
||||
|
||||
return SyncEngine.prototype._reconcile.call(this, item);
|
||||
}
|
||||
},
|
||||
|
||||
_syncFinish() {
|
||||
this.hasSyncedThisSession = true;
|
||||
return SyncEngine.prototype._syncFinish.call(this);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
|
@ -36,6 +36,7 @@ EXTRA_JS_MODULES['services-sync'] += [
|
||||
'modules/rest.js',
|
||||
'modules/service.js',
|
||||
'modules/status.js',
|
||||
'modules/SyncedTabs.jsm',
|
||||
'modules/userapi.js',
|
||||
'modules/util.js',
|
||||
]
|
||||
|
125
services/sync/tests/unit/test_syncedtabs.js
Normal file
125
services/sync/tests/unit/test_syncedtabs.js
Normal file
@ -0,0 +1,125 @@
|
||||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
|
||||
* vim:set ts=2 sw=2 sts=2 et:
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
Cu.import("resource://services-sync/main.js");
|
||||
Cu.import("resource://services-sync/SyncedTabs.jsm");
|
||||
Cu.import("resource://gre/modules/Log.jsm");
|
||||
|
||||
Log.repository.getLogger("Sync.RemoteTabs").addAppender(new Log.DumpAppender());
|
||||
|
||||
// A mock "Tabs" engine which the SyncedTabs module will use instead of the real
|
||||
// engine. We pass a constructor that Sync creates.
|
||||
function MockTabsEngine() {
|
||||
this.clients = {}; // We'll set this dynamically
|
||||
}
|
||||
|
||||
MockTabsEngine.prototype = {
|
||||
name: "tabs",
|
||||
enabled: true,
|
||||
|
||||
getAllClients() {
|
||||
return this.clients;
|
||||
},
|
||||
|
||||
getOpenURLs() {
|
||||
return new Set();
|
||||
},
|
||||
}
|
||||
|
||||
// A clients engine that doesn't need to be a constructor.
|
||||
let MockClientsEngine = {
|
||||
isMobile(guid) {
|
||||
if (!guid.endsWith("desktop") && !guid.endsWith("mobile")) {
|
||||
throw new Error("this module expected guids to end with 'desktop' or 'mobile'");
|
||||
}
|
||||
return guid.endsWith("mobile");
|
||||
},
|
||||
}
|
||||
|
||||
// Configure Sync with our mock tabs engine and force it to become initialized.
|
||||
Services.prefs.setCharPref("services.sync.username", "someone@somewhere.com");
|
||||
|
||||
Weave.Service.engineManager.unregister("tabs");
|
||||
Weave.Service.engineManager.register(MockTabsEngine);
|
||||
Weave.Service.clientsEngine = MockClientsEngine;
|
||||
|
||||
// Tell the Sync XPCOM service it is initialized.
|
||||
let weaveXPCService = Cc["@mozilla.org/weave/service;1"]
|
||||
.getService(Ci.nsISupports)
|
||||
.wrappedJSObject;
|
||||
weaveXPCService.ready = true;
|
||||
|
||||
function configureClients(clients) {
|
||||
// Configure the instance Sync created.
|
||||
let engine = Weave.Service.engineManager.get("tabs");
|
||||
// each client record is expected to have an id.
|
||||
for (let [guid, client] in Iterator(clients)) {
|
||||
client.id = guid;
|
||||
}
|
||||
engine.clients = clients;
|
||||
// Send an observer that pretends the engine just finished a sync.
|
||||
Services.obs.notifyObservers(null, "weave:engine:sync:finish", "tabs");
|
||||
}
|
||||
|
||||
// The tests.
|
||||
add_task(function* test_noClients() {
|
||||
// no clients, can't be tabs.
|
||||
yield configureClients({});
|
||||
|
||||
let tabs = yield SyncedTabs.getTabClients();
|
||||
equal(Object.keys(tabs).length, 0);
|
||||
});
|
||||
|
||||
add_task(function* test_clientWithTabs() {
|
||||
yield configureClients({
|
||||
guid_desktop: {
|
||||
clientName: "My Desktop",
|
||||
tabs: [
|
||||
{
|
||||
urlHistory: ["http://foo.com/"],
|
||||
}],
|
||||
},
|
||||
guid_mobile: {
|
||||
clientName: "My Phone",
|
||||
tabs: [],
|
||||
}
|
||||
});
|
||||
|
||||
let clients = yield SyncedTabs.getTabClients();
|
||||
equal(clients.length, 2);
|
||||
clients.sort((a, b) => { return a.name.localeCompare(b.name);});
|
||||
equal(clients[0].tabs.length, 1);
|
||||
equal(clients[0].tabs[0].url, "http://foo.com/");
|
||||
// second client has no tabs.
|
||||
equal(clients[1].tabs.length, 0);
|
||||
});
|
||||
|
||||
add_task(function* test_filter() {
|
||||
// Nothing matches.
|
||||
yield configureClients({
|
||||
guid_desktop: {
|
||||
clientName: "My Desktop",
|
||||
tabs: [
|
||||
{
|
||||
urlHistory: ["http://foo.com/"],
|
||||
title: "A test page.",
|
||||
},
|
||||
{
|
||||
urlHistory: ["http://bar.com/"],
|
||||
title: "Another page.",
|
||||
}],
|
||||
},
|
||||
});
|
||||
|
||||
let clients = yield SyncedTabs.getTabClients("foo");
|
||||
equal(clients.length, 1);
|
||||
equal(clients[0].tabs.length, 1);
|
||||
equal(clients[0].tabs[0].url, "http://foo.com/");
|
||||
// check it matches the title.
|
||||
clients = yield SyncedTabs.getTabClients("test");
|
||||
equal(clients.length, 1);
|
||||
equal(clients[0].tabs.length, 1);
|
||||
equal(clients[0].tabs[0].url, "http://foo.com/");
|
||||
});
|
@ -181,3 +181,6 @@ skip-if = ! healthreport
|
||||
|
||||
# FxA migration
|
||||
[test_fxa_migration.js]
|
||||
|
||||
# Synced tabs.
|
||||
[test_syncedtabs.js]
|
||||
|
Loading…
Reference in New Issue
Block a user