UrlbarProviderRemoteTabs.sys.mjs (7740B)
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 /** 6 * This module exports a provider that offers remote tabs. 7 */ 8 9 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 10 11 import { 12 UrlbarProvider, 13 UrlbarUtils, 14 } from "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs"; 15 16 const lazy = {}; 17 18 ChromeUtils.defineESModuleGetters(lazy, { 19 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", 20 SyncedTabs: "resource://services-sync/SyncedTabs.sys.mjs", 21 UrlbarPrefs: "moz-src:///browser/components/urlbar/UrlbarPrefs.sys.mjs", 22 UrlbarResult: "moz-src:///browser/components/urlbar/UrlbarResult.sys.mjs", 23 UrlbarTokenizer: 24 "moz-src:///browser/components/urlbar/UrlbarTokenizer.sys.mjs", 25 }); 26 27 // By default, we add remote tabs that have been used more recently than this 28 // time ago. Any remaining remote tabs are added in queue if no other results 29 // are found. 30 const RECENT_REMOTE_TAB_THRESHOLD_MS = 72 * 60 * 60 * 1000; // 72 hours. 31 32 ChromeUtils.defineLazyGetter(lazy, "weaveXPCService", function () { 33 try { 34 return Cc["@mozilla.org/weave/service;1"].getService(Ci.nsISupports) 35 .wrappedJSObject; 36 } catch (ex) { 37 // The app didn't build Sync. 38 } 39 return null; 40 }); 41 42 XPCOMUtils.defineLazyPreferenceGetter( 43 lazy, 44 "showRemoteIconsPref", 45 "services.sync.syncedTabs.showRemoteIcons", 46 true 47 ); 48 49 XPCOMUtils.defineLazyPreferenceGetter( 50 lazy, 51 "syncUsernamePref", 52 "services.sync.username" 53 ); 54 55 // from MDN... 56 function escapeRegExp(string) { 57 return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); 58 } 59 60 /** 61 * Singleton class to cache the latest remote tab data. 62 */ 63 class _cache { 64 /** @type {{tab: object, client: object}[]} */ 65 #tabsData = null; 66 67 constructor() { 68 Services.obs.addObserver( 69 this.observe.bind(this), 70 "weave:engine:sync:finish" 71 ); 72 Services.obs.addObserver( 73 this.observe.bind(this), 74 "weave:service:start-over" 75 ); 76 } 77 78 /** 79 * Build the in-memory structure we use. 80 */ 81 async #buildItems() { 82 // This is sorted by most recent client, most recent tab. 83 let tabsData = []; 84 // If Sync isn't initialized (either due to lag at startup or due to no user 85 // being signed in), don't reach in to Weave.Service as that may initialize 86 // Sync unnecessarily - we'll get an observer notification later when it 87 // becomes ready and has synced a list of tabs. 88 if (lazy.weaveXPCService.ready) { 89 let clients = await lazy.SyncedTabs.getTabClients(); 90 lazy.SyncedTabs.sortTabClientsByLastUsed(clients); 91 for (let client of clients) { 92 for (let tab of client.tabs) { 93 tabsData.push({ tab, client }); 94 } 95 } 96 } 97 this.#tabsData = tabsData; 98 } 99 100 observe(subject, topic, data) { 101 switch (topic) { 102 case "weave:engine:sync:finish": 103 if (data == "tabs") { 104 // The tabs engine just finished syncing, so may have a different list 105 // of tabs then we previously cached. 106 this.#tabsData = null; 107 } 108 break; 109 case "weave:service:start-over": 110 // Sync is being reset due to the user disconnecting - we must invalidate 111 // the cache so we don't supply tabs from a different user. 112 this.#tabsData = null; 113 break; 114 default: 115 break; 116 } 117 } 118 119 /** @type {?_cache} */ 120 static #instance; 121 /** 122 * Build (if necessary) and return tabs data. 123 * 124 * @returns {Promise<{tab: object, client: object}[]>} 125 */ 126 static async get() { 127 _cache.#instance ??= new _cache(); 128 129 if (!_cache.#instance.#tabsData) { 130 await _cache.#instance.#buildItems(); 131 } 132 return _cache.#instance.#tabsData; 133 } 134 } 135 136 /** 137 * Class used to create the provider. 138 */ 139 export class UrlbarProviderRemoteTabs extends UrlbarProvider { 140 constructor() { 141 super(); 142 } 143 144 /** 145 * @returns {Values<typeof UrlbarUtils.PROVIDER_TYPE>} 146 */ 147 get type() { 148 return UrlbarUtils.PROVIDER_TYPE.NETWORK; 149 } 150 151 /** 152 * Whether this provider should be invoked for the given context. 153 * If this method returns false, the providers manager won't start a query 154 * with this provider, to save on resources. 155 * 156 * @param {UrlbarQueryContext} queryContext The query context object 157 */ 158 async isActive(queryContext) { 159 return ( 160 lazy.syncUsernamePref && 161 lazy.UrlbarPrefs.get("suggest.remotetab") && 162 queryContext.sources.includes(UrlbarUtils.RESULT_SOURCE.TABS) && 163 lazy.weaveXPCService && 164 lazy.weaveXPCService.ready && 165 lazy.weaveXPCService.enabled 166 ); 167 } 168 169 /** 170 * Starts querying. 171 * 172 * @param {UrlbarQueryContext} queryContext 173 * @param {(provider: UrlbarProvider, result: UrlbarResult) => void} addCallback 174 * Callback invoked by the provider to add a new result. 175 */ 176 async startQuery(queryContext, addCallback) { 177 let instance = this.queryInstance; 178 179 let searchString = queryContext.tokens.map(t => t.value).join(" "); 180 181 let re = new RegExp(escapeRegExp(searchString), "i"); 182 let tabsData = await _cache.get(); 183 if (instance != this.queryInstance) { 184 return; 185 } 186 187 let resultsAdded = 0; 188 let staleTabs = []; 189 for (let { tab, client } of tabsData) { 190 if ( 191 !searchString || 192 searchString == lazy.UrlbarTokenizer.RESTRICT.OPENPAGE || 193 re.test(tab.url) || 194 (tab.title && re.test(tab.title)) 195 ) { 196 if (lazy.showRemoteIconsPref) { 197 if (!tab.icon) { 198 // It's rare that Sync supplies the icon for the page. If it does, it is a 199 // string URL. 200 tab.icon = UrlbarUtils.getIconForUrl(tab.url); 201 } else { 202 tab.icon = lazy.PlacesUtils.favicons.getFaviconLinkForIcon( 203 Services.io.newURI(tab.icon) 204 ).spec; 205 } 206 } 207 208 let result = new lazy.UrlbarResult({ 209 type: UrlbarUtils.RESULT_TYPE.REMOTE_TAB, 210 source: UrlbarUtils.RESULT_SOURCE.TABS, 211 payload: { 212 url: tab.url, 213 title: tab.title, 214 device: client.name, 215 icon: lazy.showRemoteIconsPref ? tab.icon : "", 216 lastUsed: (tab.lastUsed || 0) * 1000, 217 }, 218 highlights: { 219 url: UrlbarUtils.HIGHLIGHT.TYPED, 220 title: UrlbarUtils.HIGHLIGHT.TYPED, 221 }, 222 }); 223 224 // We want to return the most relevant remote tabs and thus the most 225 // recent ones. While SyncedTabs.sys.mjs returns tabs that are sorted by 226 // most recent client, then most recent tab, we can do better. For 227 // example, the most recent client might have one recent tab and then 228 // many very stale tabs. Those very stale tabs will push out more recent 229 // tabs from staler clients. This provider first returns tabs from the 230 // last 72 hours, sorted by client recency. Then, it adds remaining 231 // tabs. We are not concerned about filling the remote tabs group with 232 // stale tabs, because the muxer ensures remote tabs flex with other 233 // results. It will only show the stale tabs if it has nothing else 234 // to show. 235 if ( 236 tab.lastUsed <= 237 (Date.now() - RECENT_REMOTE_TAB_THRESHOLD_MS) / 1000 238 ) { 239 staleTabs.push(result); 240 } else { 241 addCallback(this, result); 242 resultsAdded++; 243 } 244 } 245 246 if (resultsAdded == queryContext.maxResults) { 247 break; 248 } 249 } 250 251 while (staleTabs.length && resultsAdded < queryContext.maxResults) { 252 addCallback(this, staleTabs.shift()); 253 resultsAdded++; 254 } 255 } 256 }