1/* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5"use strict"; 6 7var EXPORTED_SYMBOLS = ["SyncedTabs"]; 8 9const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); 10const { XPCOMUtils } = ChromeUtils.import( 11 "resource://gre/modules/XPCOMUtils.jsm" 12); 13const { Log } = ChromeUtils.import("resource://gre/modules/Log.jsm"); 14const { Weave } = ChromeUtils.import("resource://services-sync/main.js"); 15const { Preferences } = ChromeUtils.import( 16 "resource://gre/modules/Preferences.jsm" 17); 18 19// The Sync XPCOM service 20XPCOMUtils.defineLazyGetter(this, "weaveXPCService", function() { 21 return Cc["@mozilla.org/weave/service;1"].getService( 22 Ci.nsISupports 23 ).wrappedJSObject; 24}); 25 26// from MDN... 27function escapeRegExp(string) { 28 return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); 29} 30 31// A topic we fire whenever we have new tabs available. This might be due 32// to a request made by this module to refresh the tab list, or as the result 33// of a regularly scheduled sync. The intent is that consumers just listen 34// for this notification and update their UI in response. 35const TOPIC_TABS_CHANGED = "services.sync.tabs.changed"; 36 37// The interval, in seconds, before which we consider the existing list 38// of tabs "fresh enough" and don't force a new sync. 39const TABS_FRESH_ENOUGH_INTERVAL = 30; 40 41XPCOMUtils.defineLazyGetter(this, "log", function() { 42 let log = Log.repository.getLogger("Sync.RemoteTabs"); 43 log.manageLevelFromPref("services.sync.log.logger.tabs"); 44 return log; 45}); 46 47// A private singleton that does the work. 48let SyncedTabsInternal = { 49 /* Make a "tab" record. Returns a promise */ 50 async _makeTab(client, tab, url, showRemoteIcons) { 51 let icon; 52 if (showRemoteIcons) { 53 icon = tab.icon; 54 } 55 if (!icon) { 56 // By not specifying a size the favicon service will pick the default, 57 // that is usually set through setDefaultIconURIPreferredSize by the 58 // first browser window. Commonly it's 16px at current dpi. 59 icon = "page-icon:" + url; 60 } 61 return { 62 type: "tab", 63 title: tab.title || url, 64 url, 65 icon, 66 client: client.id, 67 lastUsed: tab.lastUsed, 68 }; 69 }, 70 71 /* Make a "client" record. Returns a promise for consistency with _makeTab */ 72 async _makeClient(client) { 73 return { 74 id: client.id, 75 type: "client", 76 name: Weave.Service.clientsEngine.getClientName(client.id), 77 clientType: Weave.Service.clientsEngine.getClientType(client.id), 78 lastModified: client.lastModified * 1000, // sec to ms 79 tabs: [], 80 }; 81 }, 82 83 _tabMatchesFilter(tab, filter) { 84 let reFilter = new RegExp(escapeRegExp(filter), "i"); 85 return reFilter.test(tab.url) || reFilter.test(tab.title); 86 }, 87 88 async getTabClients(filter) { 89 log.info("Generating tab list with filter", filter); 90 let result = []; 91 92 // If Sync isn't ready, don't try and get anything. 93 if (!weaveXPCService.ready) { 94 log.debug("Sync isn't yet ready, so returning an empty tab list"); 95 return result; 96 } 97 98 // A boolean that controls whether we should show the icon from the remote tab. 99 const showRemoteIcons = Preferences.get( 100 "services.sync.syncedTabs.showRemoteIcons", 101 true 102 ); 103 104 let engine = Weave.Service.engineManager.get("tabs"); 105 106 let ntabs = 0; 107 108 for (let client of Object.values(engine.getAllClients())) { 109 if (!Weave.Service.clientsEngine.remoteClientExists(client.id)) { 110 continue; 111 } 112 let clientRepr = await this._makeClient(client); 113 log.debug("Processing client", clientRepr); 114 115 for (let tab of client.tabs) { 116 let url = tab.urlHistory[0]; 117 log.trace("remote tab", url); 118 119 if (!url) { 120 continue; 121 } 122 let tabRepr = await this._makeTab(client, tab, url, showRemoteIcons); 123 if (filter && !this._tabMatchesFilter(tabRepr, filter)) { 124 continue; 125 } 126 clientRepr.tabs.push(tabRepr); 127 } 128 // We return all clients, even those without tabs - the consumer should 129 // filter it if they care. 130 ntabs += clientRepr.tabs.length; 131 result.push(clientRepr); 132 } 133 log.info(`Final tab list has ${result.length} clients with ${ntabs} tabs.`); 134 return result; 135 }, 136 137 async syncTabs(force) { 138 if (!force) { 139 // Don't bother refetching tabs if we already did so recently 140 let lastFetch = Preferences.get("services.sync.lastTabFetch", 0); 141 let now = Math.floor(Date.now() / 1000); 142 if (now - lastFetch < TABS_FRESH_ENOUGH_INTERVAL) { 143 log.info("_refetchTabs was done recently, do not doing it again"); 144 return false; 145 } 146 } 147 148 // If Sync isn't configured don't try and sync, else we will get reports 149 // of a login failure. 150 if (Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED) { 151 log.info("Sync client is not configured, so not attempting a tab sync"); 152 return false; 153 } 154 // Ask Sync to just do the tabs engine if it can. 155 try { 156 log.info("Doing a tab sync."); 157 await Weave.Service.sync({ why: "tabs", engines: ["tabs"] }); 158 return true; 159 } catch (ex) { 160 log.error("Sync failed", ex); 161 throw ex; 162 } 163 }, 164 165 observe(subject, topic, data) { 166 log.trace(`observed topic=${topic}, data=${data}, subject=${subject}`); 167 switch (topic) { 168 case "weave:engine:sync:finish": 169 if (data != "tabs") { 170 return; 171 } 172 // The tabs engine just finished syncing 173 // Set our lastTabFetch pref here so it tracks both explicit sync calls 174 // and normally scheduled ones. 175 Preferences.set( 176 "services.sync.lastTabFetch", 177 Math.floor(Date.now() / 1000) 178 ); 179 Services.obs.notifyObservers(null, TOPIC_TABS_CHANGED); 180 break; 181 case "weave:service:start-over": 182 // start-over needs to notify so consumers find no tabs. 183 Preferences.reset("services.sync.lastTabFetch"); 184 Services.obs.notifyObservers(null, TOPIC_TABS_CHANGED); 185 break; 186 case "nsPref:changed": 187 Services.obs.notifyObservers(null, TOPIC_TABS_CHANGED); 188 break; 189 default: 190 break; 191 } 192 }, 193 194 // Returns true if Sync is configured to Sync tabs, false otherwise 195 get isConfiguredToSyncTabs() { 196 if (!weaveXPCService.ready) { 197 log.debug("Sync isn't yet ready; assuming tab engine is enabled"); 198 return true; 199 } 200 201 let engine = Weave.Service.engineManager.get("tabs"); 202 return engine && engine.enabled; 203 }, 204 205 get hasSyncedThisSession() { 206 let engine = Weave.Service.engineManager.get("tabs"); 207 return engine && engine.hasSyncedThisSession; 208 }, 209}; 210 211Services.obs.addObserver(SyncedTabsInternal, "weave:engine:sync:finish"); 212Services.obs.addObserver(SyncedTabsInternal, "weave:service:start-over"); 213// Observe the pref the indicates the state of the tabs engine has changed. 214// This will force consumers to re-evaluate the state of sync and update 215// accordingly. 216Services.prefs.addObserver("services.sync.engine.tabs", SyncedTabsInternal); 217 218// The public interface. 219var SyncedTabs = { 220 // A mock-point for tests. 221 _internal: SyncedTabsInternal, 222 223 // We make the topic for the observer notification public. 224 TOPIC_TABS_CHANGED, 225 226 // Returns true if Sync is configured to Sync tabs, false otherwise 227 get isConfiguredToSyncTabs() { 228 return this._internal.isConfiguredToSyncTabs; 229 }, 230 231 // Returns true if a tab sync has completed once this session. If this 232 // returns false, then getting back no clients/tabs possibly just means we 233 // are waiting for that first sync to complete. 234 get hasSyncedThisSession() { 235 return this._internal.hasSyncedThisSession; 236 }, 237 238 // Return a promise that resolves with an array of client records, each with 239 // a .tabs array. Note that part of the contract for this module is that the 240 // returned objects are not shared between invocations, so callers are free 241 // to mutate the returned objects (eg, sort, truncate) however they see fit. 242 getTabClients(query) { 243 return this._internal.getTabClients(query); 244 }, 245 246 // Starts a background request to start syncing tabs. Returns a promise that 247 // resolves when the sync is complete, but there's no resolved value - 248 // callers should be listening for TOPIC_TABS_CHANGED. 249 // If |force| is true we always sync. If false, we only sync if the most 250 // recent sync wasn't "recently". 251 syncTabs(force) { 252 return this._internal.syncTabs(force); 253 }, 254 255 sortTabClientsByLastUsed(clients) { 256 // First sort the list of tabs for each client. Note that 257 // this module promises that the objects it returns are never 258 // shared, so we are free to mutate those objects directly. 259 for (let client of clients) { 260 let tabs = client.tabs; 261 tabs.sort((a, b) => b.lastUsed - a.lastUsed); 262 } 263 // Now sort the clients - the clients are sorted in the order of the 264 // most recent tab for that client (ie, it is important the tabs for 265 // each client are already sorted.) 266 clients.sort((a, b) => { 267 if (a.tabs.length == 0) { 268 return 1; // b comes first. 269 } 270 if (b.tabs.length == 0) { 271 return -1; // a comes first. 272 } 273 return b.tabs[0].lastUsed - a.tabs[0].lastUsed; 274 }); 275 }, 276}; 277