WindowsJumpLists.sys.mjs (16575B)
1 /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ 2 /* This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs"; 7 8 // Stop updating jumplists after some idle time. 9 const IDLE_TIMEOUT_SECONDS = 5 * 60; 10 11 // Prefs 12 const PREF_TASKBAR_BRANCH = "browser.taskbar.lists."; 13 const PREF_TASKBAR_ENABLED = "enabled"; 14 const PREF_TASKBAR_ITEMCOUNT = "maxListItemCount"; 15 const PREF_TASKBAR_FREQUENT = "frequent.enabled"; 16 const PREF_TASKBAR_RECENT = "recent.enabled"; 17 const PREF_TASKBAR_TASKS = "tasks.enabled"; 18 const PREF_TASKBAR_REFRESH = "refreshInSeconds"; 19 20 /** 21 * Exports 22 */ 23 24 const lazy = {}; 25 26 /** 27 * Smart getters 28 */ 29 30 ChromeUtils.defineLazyGetter(lazy, "_prefs", function () { 31 return Services.prefs.getBranch(PREF_TASKBAR_BRANCH); 32 }); 33 34 ChromeUtils.defineLazyGetter(lazy, "_stringBundle", function () { 35 return Services.strings.createBundle( 36 "chrome://browser/locale/taskbar.properties" 37 ); 38 }); 39 40 ChromeUtils.defineLazyGetter(lazy, "logConsole", function () { 41 return console.createInstance({ 42 prefix: "WindowsJumpLists", 43 maxLogLevel: Services.prefs.getBoolPref("browser.taskbar.log", false) 44 ? "Debug" 45 : "Warn", 46 }); 47 }); 48 49 XPCOMUtils.defineLazyServiceGetter( 50 lazy, 51 "_idle", 52 "@mozilla.org/widget/useridleservice;1", 53 Ci.nsIUserIdleService 54 ); 55 XPCOMUtils.defineLazyServiceGetter( 56 lazy, 57 "_taskbarService", 58 "@mozilla.org/windows-taskbar;1", 59 Ci.nsIWinTaskbar 60 ); 61 62 ChromeUtils.defineESModuleGetters(lazy, { 63 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", 64 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 65 }); 66 67 /** 68 * Global functions 69 */ 70 71 function _getString(name) { 72 return lazy._stringBundle.GetStringFromName(name); 73 } 74 75 // Task list configuration data object. 76 77 var tasksCfg = [ 78 /** 79 * Task configuration options: title, description, args, iconIndex, open, close. 80 * 81 * title - Task title displayed in the list. (strings in the table are temp fillers.) 82 * description - Tooltip description on the list item. 83 * args - Command line args to invoke the task. 84 * iconIndex - Optional win icon index into the main application for the 85 * list item. 86 * open - Boolean indicates if the command should be visible after the browser opens. 87 * close - Boolean indicates if the command should be visible after the browser closes. 88 */ 89 // Open new tab 90 { 91 get title() { 92 return _getString("taskbar.tasks.newTab.label"); 93 }, 94 get description() { 95 return _getString("taskbar.tasks.newTab.description"); 96 }, 97 args: "-new-tab about:blank", 98 iconIndex: 3, // New window icon 99 open: true, 100 close: true, // The jump list already has an app launch icon, but 101 // we don't always update the list on shutdown. 102 // Thus true for consistency. 103 }, 104 105 // Open new window 106 { 107 get title() { 108 return _getString("taskbar.tasks.newWindow.label"); 109 }, 110 get description() { 111 return _getString("taskbar.tasks.newWindow.description"); 112 }, 113 args: "-browser", 114 iconIndex: 2, // New tab icon 115 open: true, 116 close: true, // No point, but we don't always update the list on 117 // shutdown. Thus true for consistency. 118 }, 119 ]; 120 121 // Open new private window 122 let privateWindowTask = { 123 get title() { 124 return _getString("taskbar.tasks.newPrivateWindow.label"); 125 }, 126 get description() { 127 return _getString("taskbar.tasks.newPrivateWindow.description"); 128 }, 129 args: "-private-window", 130 iconIndex: 4, // Private browsing mode icon 131 open: true, 132 close: true, // No point, but we don't always update the list on 133 // shutdown. Thus true for consistency. 134 }; 135 136 // Implementation 137 138 var Builder = class { 139 constructor(builder) { 140 this._builder = builder; 141 this._tasks = null; 142 this._shuttingDown = false; 143 // These are ultimately controlled by prefs, so we disable 144 // everything until is read from there 145 this._showTasks = false; 146 this._showFrequent = false; 147 this._showRecent = false; 148 this._maxItemCount = 0; 149 this._isBuilding = false; 150 } 151 152 refreshPrefs(showTasks, showFrequent, showRecent, maxItemCount) { 153 this._showTasks = showTasks; 154 this._showFrequent = showFrequent; 155 this._showRecent = showRecent; 156 this._maxItemCount = maxItemCount; 157 } 158 159 updateShutdownState(shuttingDown) { 160 this._shuttingDown = shuttingDown; 161 } 162 163 delete() { 164 delete this._builder; 165 } 166 167 /** 168 * Constructs the tasks and recent history items to display in the JumpList, 169 * and then sends those lists to the nsIJumpListBuilder to be written. 170 * 171 * @returns {Promise<undefined>} 172 * The Promise resolves once the JumpList has been written, and any 173 * items that the user remove from the recent history list have been 174 * removed from Places. The Promise may reject if any part of constructing 175 * the tasks or sending them to the builder thread failed. 176 */ 177 async buildList() { 178 if (!(this._builder instanceof Ci.nsIJumpListBuilder)) { 179 console.error( 180 "Expected nsIJumpListBuilder. The builder is of the wrong type." 181 ); 182 return; 183 } 184 185 // anything to build? 186 if (!this._showFrequent && !this._showRecent && !this._showTasks) { 187 // don't leave the last list hanging on the taskbar. 188 this._deleteActiveJumpList(); 189 return; 190 } 191 192 // Are we in the midst of building an earlier iteration of this list? If 193 // so, bail out. Same if we're shutting down. 194 if (this._isBuilding || this._shuttingDown) { 195 return; 196 } 197 198 this._isBuilding = true; 199 200 try { 201 let removedURLs = await this._builder.checkForRemovals(); 202 if (removedURLs.length) { 203 await this._clearHistory(removedURLs); 204 } 205 206 let selfPath = Services.dirsvc.get("XREExeF", Ci.nsIFile).path; 207 208 let taskDescriptions = []; 209 210 if (this._showTasks) { 211 taskDescriptions = this._tasks.map(task => { 212 return { 213 title: task.title, 214 description: task.description, 215 path: selfPath, 216 arguments: task.args, 217 fallbackIconIndex: task.iconIndex, 218 }; 219 }); 220 } 221 222 let customTitle = ""; 223 let customDescriptions = []; 224 225 if (this._showFrequent) { 226 let conn = await lazy.PlacesUtils.promiseDBConnection(); 227 let rows = await conn.executeCached( 228 "SELECT p.url, IFNULL(p.title, p.url) as title " + 229 "FROM moz_places p WHERE p.hidden = 0 " + 230 "AND EXISTS (" + 231 "SELECT id FROM moz_historyvisits WHERE " + 232 "place_id = p.id AND " + 233 "visit_type NOT IN (" + 234 "0, " + 235 `${Ci.nsINavHistoryService.TRANSITION_EMBED}, ` + 236 `${Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK}` + 237 ")" + 238 "LIMIT 1" + 239 ") " + 240 "ORDER BY p.visit_count DESC LIMIT :limit", 241 { 242 limit: this._maxItemCount, 243 } 244 ); 245 246 for (let row of rows) { 247 let uri = Services.io.newURI(row.getResultByName("url")); 248 let iconPath = ""; 249 try { 250 iconPath = await this._builder.obtainAndCacheFaviconAsync(uri); 251 } catch (e) { 252 // obtainAndCacheFaviconAsync may throw NS_ERROR_NOT_AVAILABLE if 253 // the icon doesn't yet exist on the disk, but has been requested. 254 // It might also throw an exception if there was a problem fetching 255 // the favicon from the database and writing it to the disk. Either 256 // case is non-fatal, so we ignore them here. 257 lazy.logConsole.warn("Failed to fetch favicon for ", uri.spec, e); 258 } 259 260 customDescriptions.push({ 261 title: row.getResultByName("title"), 262 description: row.getResultByName("title"), 263 path: selfPath, 264 arguments: row.getResultByName("url"), 265 fallbackIconIndex: 1, 266 iconPath, 267 }); 268 } 269 270 customTitle = _getString("taskbar.frequent.label"); 271 } 272 273 if (!this._shuttingDown) { 274 await this._builder.populateJumpList( 275 taskDescriptions, 276 customTitle, 277 customDescriptions 278 ); 279 } 280 } catch (e) { 281 console.error("buildList failed: ", e); 282 } finally { 283 this._isBuilding = false; 284 } 285 } 286 287 _deleteActiveJumpList() { 288 this._builder.clearJumpList(); 289 } 290 291 /** 292 * Removes URLs from history in Places that the user has requested to clear 293 * from their Jump List. We must do this before recomputing which history 294 * to put into the Jump List, because if we ever include items that have 295 * recently been removed, Windows will not allow us to proceed. 296 * Please see 297 * https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-icustomdestinationlist-beginlist 298 * for more details. 299 * 300 * The returned Promise never rejects, but may report console errors in the 301 * event of removal failure. 302 * 303 * @param {string[]} uriSpecsToRemove 304 * The URLs to be removed from Places history. 305 * @returns {Promise<undefined>} 306 */ 307 _clearHistory(uriSpecsToRemove) { 308 let URIsToRemove = uriSpecsToRemove 309 .map(spec => URL.parse(spec)?.URI) 310 .filter(uri => !!uri); 311 312 if (URIsToRemove.length) { 313 return lazy.PlacesUtils.history.remove(URIsToRemove).catch(console.error); 314 } 315 return Promise.resolve(); 316 } 317 }; 318 319 export var WinTaskbarJumpList = { 320 // We build two separate jump lists -- one for the regular Firefox icon 321 // and one for the Private Browsing icon 322 _builder: null, 323 _pbBuilder: null, 324 _builtPb: false, 325 // Is showing jump lists currently blocked, such as when waiting for the user 326 // to interact with the preonboarding modal? 327 _blocked: false, 328 _shuttingDown: false, 329 330 /** 331 * Startup, shutdown, and update 332 */ 333 334 startup: async function WTBJL_startup() { 335 if (!lazy._taskbarService.available) { 336 return; 337 } 338 // exit if initting the taskbar failed for some reason. 339 if (!(await this._initTaskbar())) { 340 return; 341 } 342 343 if (lazy.PrivateBrowsingUtils.enabled) { 344 tasksCfg.push(privateWindowTask); 345 } 346 // Store our task list config data 347 this._builder._tasks = tasksCfg; 348 this._pbBuilder._tasks = tasksCfg; 349 350 // retrieve taskbar related prefs. 351 this._refreshPrefs(); 352 353 // observer for private browsing and our prefs branch 354 this._initObs(); 355 356 // jump list refresh timer 357 this._updateTimer(); 358 359 if (this._blocked) { 360 this._builder._deleteActiveJumpList(); 361 } 362 }, 363 364 update: function WTBJL_update() { 365 // are we disabled via prefs or currently blocked? don't do anything! 366 if (!this._enabled || this._blocked) { 367 return; 368 } 369 370 if (this._shuttingDown) { 371 return; 372 } 373 374 this._builder.buildList(); 375 376 // We only ever need to do this once because the private browsing window 377 // jumplist only ever shows the static task list, which never changes, 378 // so it doesn't need to be updated over time. 379 if (!this._builtPb) { 380 this._pbBuilder.buildList(); 381 this._builtPb = true; 382 } 383 }, 384 385 _shutdown: function WTBJL__shutdown() { 386 this._builder.updateShutdownState(true); 387 this._pbBuilder.updateShutdownState(true); 388 this._shuttingDown = true; 389 this._free(); 390 }, 391 392 /** 393 * Prefs utilities 394 */ 395 396 _refreshPrefs: function WTBJL__refreshPrefs() { 397 this._enabled = lazy._prefs.getBoolPref(PREF_TASKBAR_ENABLED); 398 var showTasks = lazy._prefs.getBoolPref(PREF_TASKBAR_TASKS); 399 this._builder.refreshPrefs( 400 showTasks, 401 lazy._prefs.getBoolPref(PREF_TASKBAR_FREQUENT), 402 lazy._prefs.getBoolPref(PREF_TASKBAR_RECENT), 403 lazy._prefs.getIntPref(PREF_TASKBAR_ITEMCOUNT) 404 ); 405 // showTasks is the only relevant pref for the Private Browsing Jump List 406 // the others are are related to frequent/recent entries, which are 407 // explicitly disabled for it 408 this._pbBuilder.refreshPrefs(showTasks, false, false, 0); 409 }, 410 411 /** 412 * Init and shutdown utilities 413 */ 414 415 _initTaskbar: async function WTBJL__initTaskbar() { 416 let builder; 417 let pbBuilder; 418 419 builder = lazy._taskbarService.createJumpListBuilder(false); 420 pbBuilder = lazy._taskbarService.createJumpListBuilder(true); 421 if (!builder || !pbBuilder) { 422 return false; 423 } 424 let [builderAvailable, pbBuilderAvailable] = await Promise.all([ 425 builder.isAvailable(), 426 pbBuilder.isAvailable(), 427 ]); 428 if (!builderAvailable || !pbBuilderAvailable) { 429 return false; 430 } 431 432 this._builder = new Builder(builder); 433 this._pbBuilder = new Builder(pbBuilder); 434 435 return true; 436 }, 437 438 _initObs: function WTBJL__initObs() { 439 // If the browser is closed while in private browsing mode, the "exit" 440 // notification is fired on quit-application-granted. 441 // History cleanup can happen at profile-change-teardown. 442 Services.obs.addObserver(this, "profile-before-change"); 443 Services.obs.addObserver(this, "browser:purge-session-history"); 444 lazy._prefs.addObserver("", this); 445 this._placesObserver = new PlacesWeakCallbackWrapper( 446 this.update.bind(this) 447 ); 448 lazy.PlacesUtils.observers.addListener( 449 ["history-cleared"], 450 this._placesObserver 451 ); 452 }, 453 454 _freeObs: function WTBJL__freeObs() { 455 Services.obs.removeObserver(this, "profile-before-change"); 456 Services.obs.removeObserver(this, "browser:purge-session-history"); 457 lazy._prefs.removeObserver("", this); 458 if (this._placesObserver) { 459 lazy.PlacesUtils.observers.removeListener( 460 ["history-cleared"], 461 this._placesObserver 462 ); 463 } 464 }, 465 466 _updateTimer: function WTBJL__updateTimer() { 467 if (this._enabled && !this._shuttingDown && !this._timer) { 468 this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); 469 this._timer.initWithCallback( 470 this, 471 lazy._prefs.getIntPref(PREF_TASKBAR_REFRESH) * 1000, 472 this._timer.TYPE_REPEATING_SLACK 473 ); 474 } else if ((!this._enabled || this._shuttingDown) && this._timer) { 475 this._timer.cancel(); 476 delete this._timer; 477 } 478 }, 479 480 _hasIdleObserver: false, 481 _updateIdleObserver: function WTBJL__updateIdleObserver() { 482 if (this._enabled && !this._shuttingDown && !this._hasIdleObserver) { 483 lazy._idle.addIdleObserver(this, IDLE_TIMEOUT_SECONDS); 484 this._hasIdleObserver = true; 485 } else if ( 486 (!this._enabled || this._shuttingDown) && 487 this._hasIdleObserver 488 ) { 489 lazy._idle.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS); 490 this._hasIdleObserver = false; 491 } 492 }, 493 494 _free: function WTBJL__free() { 495 this._freeObs(); 496 this._updateTimer(); 497 this._updateIdleObserver(); 498 this._builder.delete(); 499 this._pbBuilder.delete(); 500 }, 501 502 QueryInterface: ChromeUtils.generateQI([ 503 "nsINamed", 504 "nsIObserver", 505 "nsITimerCallback", 506 ]), 507 508 name: "WinTaskbarJumpList", 509 510 blockJumpList: async function WTBJL_clearJumpList(unblockPromise) { 511 this._blocked = true; 512 if (unblockPromise) { 513 try { 514 await unblockPromise; 515 } catch (e) { 516 console.error("Unblock promise error, reinstating jump list: ", e); 517 } 518 } 519 this._unblockJumpList(); 520 }, 521 522 _unblockJumpList: function WTBJL_updateJumpList() { 523 this._blocked = false; 524 this.update(); 525 }, 526 527 notify: function WTBJL_notify() { 528 // Add idle observer on the first notification so it doesn't hit startup. 529 this._updateIdleObserver(); 530 Services.tm.idleDispatchToMainThread(() => { 531 this.update(); 532 }); 533 }, 534 535 observe: function WTBJL_observe(aSubject, aTopic) { 536 switch (aTopic) { 537 case "nsPref:changed": 538 if (this._enabled && !lazy._prefs.getBoolPref(PREF_TASKBAR_ENABLED)) { 539 this._builder._deleteActiveJumpList(); 540 } 541 this._refreshPrefs(); 542 this._updateTimer(); 543 this._updateIdleObserver(); 544 Services.tm.idleDispatchToMainThread(() => { 545 this.update(); 546 }); 547 break; 548 549 case "profile-before-change": 550 this._shutdown(); 551 break; 552 553 case "browser:purge-session-history": 554 this.update(); 555 break; 556 case "idle": 557 if (this._timer) { 558 this._timer.cancel(); 559 delete this._timer; 560 } 561 break; 562 563 case "active": 564 this._updateTimer(); 565 break; 566 } 567 }, 568 };