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/. */
6 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
8 // Stop updating jumplists after some idle time.
9 const IDLE_TIMEOUT_SECONDS = 5 * 60;
12 const PREF_TASKBAR_BRANCH = "browser.taskbar.lists.";
13 const PREF_TASKBAR_LEGACY_BACKEND = "legacyBackend";
14 const PREF_TASKBAR_ENABLED = "enabled";
15 const PREF_TASKBAR_ITEMCOUNT = "maxListItemCount";
16 const PREF_TASKBAR_FREQUENT = "frequent.enabled";
17 const PREF_TASKBAR_RECENT = "recent.enabled";
18 const PREF_TASKBAR_TASKS = "tasks.enabled";
19 const PREF_TASKBAR_REFRESH = "refreshInSeconds";
21 // Hash keys for pendingStatements.
37 ChromeUtils.defineLazyGetter(lazy, "_prefs", function () {
38 return Services.prefs.getBranch(PREF_TASKBAR_BRANCH);
41 ChromeUtils.defineLazyGetter(lazy, "_stringBundle", function () {
42 return Services.strings.createBundle(
43 "chrome://browser/locale/taskbar.properties"
47 XPCOMUtils.defineLazyServiceGetter(
50 "@mozilla.org/widget/useridleservice;1",
53 XPCOMUtils.defineLazyServiceGetter(
56 "@mozilla.org/windows-taskbar;1",
60 ChromeUtils.defineESModuleGetters(lazy, {
61 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
62 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
69 function _getString(name) {
70 return lazy._stringBundle.GetStringFromName(name);
73 // Task list configuration data object.
77 * Task configuration options: title, description, args, iconIndex, open, close.
79 * title - Task title displayed in the list. (strings in the table are temp fillers.)
80 * description - Tooltip description on the list item.
81 * args - Command line args to invoke the task.
82 * iconIndex - Optional win icon index into the main application for the
84 * open - Boolean indicates if the command should be visible after the browser opens.
85 * close - Boolean indicates if the command should be visible after the browser closes.
90 return _getString("taskbar.tasks.newTab.label");
93 return _getString("taskbar.tasks.newTab.description");
95 args: "-new-tab about:blank",
96 iconIndex: 3, // New window icon
98 close: true, // The jump list already has an app launch icon, but
99 // we don't always update the list on shutdown.
100 // Thus true for consistency.
106 return _getString("taskbar.tasks.newWindow.label");
109 return _getString("taskbar.tasks.newWindow.description");
112 iconIndex: 2, // New tab icon
114 close: true, // No point, but we don't always update the list on
115 // shutdown. Thus true for consistency.
119 // Open new private window
120 let privateWindowTask = {
122 return _getString("taskbar.tasks.newPrivateWindow.label");
125 return _getString("taskbar.tasks.newPrivateWindow.description");
127 args: "-private-window",
128 iconIndex: 4, // Private browsing mode icon
130 close: true, // No point, but we don't always update the list on
131 // shutdown. Thus true for consistency.
136 var Builder = class {
137 constructor(builder) {
138 this._builder = builder;
140 this._pendingStatements = {};
141 this._shuttingDown = false;
142 // These are ultimately controlled by prefs, so we disable
143 // everything until is read from there
144 this._showTasks = false;
145 this._showFrequent = false;
146 this._showRecent = false;
147 this._maxItemCount = 0;
148 this._isBuilding = false;
151 refreshPrefs(showTasks, showFrequent, showRecent, maxItemCount) {
152 this._showTasks = showTasks;
153 this._showFrequent = showFrequent;
154 this._showRecent = showRecent;
155 this._maxItemCount = maxItemCount;
158 updateShutdownState(shuttingDown) {
159 this._shuttingDown = shuttingDown;
163 delete this._builder;
167 * Constructs the tasks and recent history items to display in the JumpList,
168 * and then sends those lists to the nsIJumpListBuilder to be written.
170 * @returns {Promise<undefined>}
171 * The Promise resolves once the JumpList has been written, and any
172 * items that the user remove from the recent history list have been
173 * removed from Places. The Promise may reject if any part of constructing
174 * the tasks or sending them to the builder thread failed.
177 if (!(this._builder instanceof Ci.nsIJumpListBuilder)) {
179 "Expected nsIJumpListBuilder. The builder is of the wrong type."
184 // anything to build?
185 if (!this._showFrequent && !this._showRecent && !this._showTasks) {
186 // don't leave the last list hanging on the taskbar.
187 this._deleteActiveJumpList();
191 // Are we in the midst of building an earlier iteration of this list? If
192 // so, bail out. Same if we're shutting down.
193 if (this._isBuilding || this._shuttingDown) {
197 this._isBuilding = true;
200 let removedURLs = await this._builder.checkForRemovals();
201 if (removedURLs.length) {
202 await this._clearHistory(removedURLs);
205 let selfPath = Services.dirsvc.get("XREExeF", Ci.nsIFile).path;
207 let taskDescriptions = [];
209 if (this._showTasks) {
210 taskDescriptions = this._tasks.map(task => {
213 description: task.description,
215 arguments: task.args,
216 fallbackIconIndex: task.iconIndex,
221 let customTitle = "";
222 let customDescriptions = [];
224 if (this._showFrequent) {
225 let conn = await lazy.PlacesUtils.promiseDBConnection();
226 let rows = await conn.executeCached(
227 "SELECT p.url, IFNULL(p.title, p.url) as title " +
228 "FROM moz_places p WHERE p.hidden = 0 " +
230 "SELECT id FROM moz_historyvisits WHERE " +
231 "place_id = p.id AND " +
232 "visit_type NOT IN (" +
234 `${Ci.nsINavHistoryService.TRANSITION_EMBED}, ` +
235 `${Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK}` +
239 "ORDER BY p.visit_count DESC LIMIT :limit",
241 limit: this._maxItemCount,
245 customDescriptions = rows.map(row => {
246 let uri = Services.io.newURI(row.getResultByName("url"));
249 iconPath = this._builder.obtainAndCacheFavicon(uri);
251 // obtainAndCacheFavicon may throw NS_ERROR_NOT_AVAILABLE if the
252 // icon doesn't yet exist on the disk, but has been requested.
253 // That's not fatal, so we'll just let it pass. Any other errors,
254 // however, we'll abort on.
255 if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) {
261 title: row.getResultByName("title"),
262 description: row.getResultByName("title"),
264 arguments: row.getResultByName("url"),
265 fallbackIconIndex: 1,
270 customTitle = _getString("taskbar.frequent.label");
273 if (!this._shuttingDown) {
274 await this._builder.populateJumpList(
281 console.error("buildList failed: ", e);
283 this._isBuilding = false;
288 * Legacy list building
290 * @note Async builders must add their mozIStoragePendingStatement to
291 * _pendingStatements object, using a different LIST_TYPE entry for
292 * each statement. Once finished they must remove it and call
293 * commitBuild(). When there will be no more _pendingStatements,
294 * commitBuild() will commit for real.
297 _hasPendingStatements() {
298 return !!Object.keys(this._pendingStatements).length;
301 async buildListLegacy() {
302 if (!(this._builder instanceof Ci.nsILegacyJumpListBuilder)) {
304 "Expected nsILegacyJumpListBuilder. The builder is of the wrong type."
310 (this._showFrequent || this._showRecent) &&
311 this._hasPendingStatements()
313 // We were requested to update the list while another update was in
314 // progress, this could happen at shutdown, idle or privatebrowsing.
315 // Abort the current list building.
316 for (let listType in this._pendingStatements) {
317 this._pendingStatements[listType].cancel();
318 delete this._pendingStatements[listType];
320 this._builder.abortListBuild();
323 // anything to build?
324 if (!this._showFrequent && !this._showRecent && !this._showTasks) {
325 // don't leave the last list hanging on the taskbar.
326 this._deleteActiveJumpList();
330 await this._startBuild();
332 if (this._showTasks) {
336 // Space for frequent items takes priority over recent.
337 if (this._showFrequent) {
338 this._buildFrequent();
341 if (this._showRecent) {
349 * Taskbar api wrappers
352 async _startBuild() {
353 this._builder.abortListBuild();
354 let URIsToRemove = await this._builder.initListBuild();
355 if (URIsToRemove.length) {
356 // Prior to building, delete removed items from history.
357 this._clearHistory(URIsToRemove);
363 (this._showFrequent || this._showRecent) &&
364 this._hasPendingStatements()
369 this._builder.commitListBuild(succeed => {
371 this._builder.abortListBuild();
377 var items = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
378 this._tasks.forEach(function (task) {
380 (this._shuttingDown && !task.close) ||
381 (!this._shuttingDown && !task.open)
385 var item = this._getHandlerAppItem(
392 items.appendElement(item);
396 this._builder.addListToBuild(
397 this._builder.JUMPLIST_CATEGORY_TASKS,
403 _buildCustom(title, items) {
405 this._builder.addListToBuild(
406 this._builder.JUMPLIST_CATEGORY_CUSTOMLIST,
414 // Windows supports default frequent and recent lists,
415 // but those depend on internal windows visit tracking
416 // which we don't populate. So we build our own custom
417 // frequent and recent lists using our nav history data.
419 var items = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
420 // track frequent items so that we don't add them to
422 this._frequentHashList = [];
424 this._pendingStatements[LIST_TYPE.FREQUENT] = this._getHistoryResults(
425 Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING,
429 delete this._pendingStatements[LIST_TYPE.FREQUENT];
430 // The are no more results, build the list.
431 this._buildCustom(_getString("taskbar.frequent.label"), items);
436 let title = aResult.title || aResult.uri;
437 let faviconPageUri = Services.io.newURI(aResult.uri);
438 let shortcut = this._getHandlerAppItem(
445 items.appendElement(shortcut);
446 this._frequentHashList.push(aResult.uri);
453 var items = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
454 // Frequent items will be skipped, so we select a double amount of
455 // entries and stop fetching results at _maxItemCount.
458 this._pendingStatements[LIST_TYPE.RECENT] = this._getHistoryResults(
459 Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING,
460 this._maxItemCount * 2,
463 // The are no more results, build the list.
464 this._buildCustom(_getString("taskbar.recent.label"), items);
465 delete this._pendingStatements[LIST_TYPE.RECENT];
470 if (count >= this._maxItemCount) {
474 // Do not add items to recent that have already been added to frequent.
476 this._frequentHashList &&
477 this._frequentHashList.includes(aResult.uri)
482 let title = aResult.title || aResult.uri;
483 let faviconPageUri = Services.io.newURI(aResult.uri);
484 let shortcut = this._getHandlerAppItem(
491 items.appendElement(shortcut);
498 _deleteActiveJumpList() {
499 if (this._builder instanceof Ci.nsIJumpListBuilder) {
500 this._builder.clearJumpList();
502 this._builder.deleteActiveList();
507 * Jump list item creation helpers
510 _getHandlerAppItem(name, description, args, iconIndex, faviconPageUri) {
511 var file = Services.dirsvc.get("XREExeF", Ci.nsIFile);
514 "@mozilla.org/uriloader/local-handler-app;1"
515 ].createInstance(Ci.nsILocalHandlerApp);
516 handlerApp.executable = file;
517 // handlers default to the leaf name if a name is not specified
518 if (name && name.length) {
519 handlerApp.name = name;
521 handlerApp.detailedDescription = description;
522 handlerApp.appendParameter(args);
525 "@mozilla.org/windows-legacyjumplistshortcut;1"
526 ].createInstance(Ci.nsILegacyJumpListShortcut);
527 item.app = handlerApp;
528 item.iconIndex = iconIndex;
529 item.faviconPageUri = faviconPageUri;
534 * Nav history helpers
537 _getHistoryResults(aSortingMode, aLimit, aCallback, aScope) {
538 var options = lazy.PlacesUtils.history.getNewQueryOptions();
539 options.maxResults = aLimit;
540 options.sortingMode = aSortingMode;
541 var query = lazy.PlacesUtils.history.getNewQuery();
543 // Return the pending statement to the caller, to allow cancelation.
544 return lazy.PlacesUtils.history.asyncExecuteLegacyQuery(query, options, {
545 handleResult(aResultSet) {
546 for (let row; (row = aResultSet.getNextRow()); ) {
548 aCallback.call(aScope, {
549 uri: row.getResultByIndex(1),
550 title: row.getResultByIndex(2),
555 handleError(aError) {
557 "Async execution error (",
564 aCallback.call(aScope, null);
570 * Removes URLs from history in Places that the user has requested to clear
571 * from their Jump List. We must do this before recomputing which history
572 * to put into the Jump List, because if we ever include items that have
573 * recently been removed, Windows will not allow us to proceed.
575 * https://learn.microsoft.com/en-us/windows/win32/api/shobjidl_core/nf-shobjidl_core-icustomdestinationlist-beginlist
578 * The returned Promise never rejects, but may report console errors in the
579 * event of removal failure.
581 * @param {string[]} uriSpecsToRemove
582 * The URLs to be removed from Places history.
583 * @returns {Promise<undefined>}
585 _clearHistory(uriSpecsToRemove) {
586 let URIsToRemove = uriSpecsToRemove
589 // in case we get a bad uri
590 return Services.io.newURI(spec);
595 .filter(uri => !!uri);
597 if (URIsToRemove.length) {
598 return lazy.PlacesUtils.history.remove(URIsToRemove).catch(console.error);
600 return Promise.resolve();
604 export var WinTaskbarJumpList = {
605 // We build two separate jump lists -- one for the regular Firefox icon
606 // and one for the Private Browsing icon
610 _shuttingDown: false,
611 _useLegacyBackend: true,
614 * Startup, shutdown, and update
617 startup: async function WTBJL_startup() {
618 // We do a one-time startup read of the backend pref here because
619 // we don't want to consider any bugs that occur if the pref is flipped
620 // at runtime. We want the pref flip to only take effect on a restart.
621 this._useLegacyBackend = lazy._prefs.getBoolPref(
622 PREF_TASKBAR_LEGACY_BACKEND
625 // exit if initting the taskbar failed for some reason.
626 if (!(await this._initTaskbar())) {
630 if (lazy.PrivateBrowsingUtils.enabled) {
631 tasksCfg.push(privateWindowTask);
633 // Store our task list config data
634 this._builder._tasks = tasksCfg;
635 this._pbBuilder._tasks = tasksCfg;
637 // retrieve taskbar related prefs.
638 this._refreshPrefs();
640 // observer for private browsing and our prefs branch
643 // jump list refresh timer
647 update: function WTBJL_update() {
648 // are we disabled via prefs? don't do anything!
649 if (!this._enabled) {
653 if (this._shuttingDown) {
657 if (this._useLegacyBackend) {
658 // we only need to do this once, but we do it here
659 // to avoid main thread io on startup
660 if (!this._builtPb) {
661 this._pbBuilder.buildListLegacy();
662 this._builtPb = true;
665 // do what we came here to do, update the taskbar jumplist
666 this._builder.buildListLegacy();
668 this._builder.buildList();
670 // We only ever need to do this once because the private browsing window
671 // jumplist only ever shows the static task list, which never changes,
672 // so it doesn't need to be updated over time.
673 if (!this._builtPb) {
674 this._pbBuilder.buildList();
675 this._builtPb = true;
680 _shutdown: function WTBJL__shutdown() {
681 this._builder.updateShutdownState(true);
682 this._pbBuilder.updateShutdownState(true);
683 this._shuttingDown = true;
691 _refreshPrefs: function WTBJL__refreshPrefs() {
692 this._enabled = lazy._prefs.getBoolPref(PREF_TASKBAR_ENABLED);
693 var showTasks = lazy._prefs.getBoolPref(PREF_TASKBAR_TASKS);
694 this._builder.refreshPrefs(
696 lazy._prefs.getBoolPref(PREF_TASKBAR_FREQUENT),
697 lazy._prefs.getBoolPref(PREF_TASKBAR_RECENT),
698 lazy._prefs.getIntPref(PREF_TASKBAR_ITEMCOUNT)
700 // showTasks is the only relevant pref for the Private Browsing Jump List
701 // the others are are related to frequent/recent entries, which are
702 // explicitly disabled for it
703 this._pbBuilder.refreshPrefs(showTasks, false, false, 0);
707 * Init and shutdown utilities
710 _initTaskbar: async function WTBJL__initTaskbar() {
714 if (this._useLegacyBackend) {
715 builder = lazy._taskbarService.createLegacyJumpListBuilder(false);
716 pbBuilder = lazy._taskbarService.createLegacyJumpListBuilder(true);
719 !builder.available ||
726 builder = lazy._taskbarService.createJumpListBuilder(false);
727 pbBuilder = lazy._taskbarService.createJumpListBuilder(true);
728 if (!builder || !pbBuilder) {
731 let [builderAvailable, pbBuilderAvailable] = await Promise.all([
732 builder.isAvailable(),
733 pbBuilder.isAvailable(),
735 if (!builderAvailable || !pbBuilderAvailable) {
740 this._builder = new Builder(builder);
741 this._pbBuilder = new Builder(pbBuilder);
746 _initObs: function WTBJL__initObs() {
747 // If the browser is closed while in private browsing mode, the "exit"
748 // notification is fired on quit-application-granted.
749 // History cleanup can happen at profile-change-teardown.
750 Services.obs.addObserver(this, "profile-before-change");
751 Services.obs.addObserver(this, "browser:purge-session-history");
752 lazy._prefs.addObserver("", this);
753 this._placesObserver = new PlacesWeakCallbackWrapper(
754 this.update.bind(this)
756 lazy.PlacesUtils.observers.addListener(
762 _freeObs: function WTBJL__freeObs() {
763 Services.obs.removeObserver(this, "profile-before-change");
764 Services.obs.removeObserver(this, "browser:purge-session-history");
765 lazy._prefs.removeObserver("", this);
766 if (this._placesObserver) {
767 lazy.PlacesUtils.observers.removeListener(
774 _updateTimer: function WTBJL__updateTimer() {
775 if (this._enabled && !this._shuttingDown && !this._timer) {
776 this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
777 this._timer.initWithCallback(
779 lazy._prefs.getIntPref(PREF_TASKBAR_REFRESH) * 1000,
780 this._timer.TYPE_REPEATING_SLACK
782 } else if ((!this._enabled || this._shuttingDown) && this._timer) {
783 this._timer.cancel();
788 _hasIdleObserver: false,
789 _updateIdleObserver: function WTBJL__updateIdleObserver() {
790 if (this._enabled && !this._shuttingDown && !this._hasIdleObserver) {
791 lazy._idle.addIdleObserver(this, IDLE_TIMEOUT_SECONDS);
792 this._hasIdleObserver = true;
794 (!this._enabled || this._shuttingDown) &&
795 this._hasIdleObserver
797 lazy._idle.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS);
798 this._hasIdleObserver = false;
802 _free: function WTBJL__free() {
805 this._updateIdleObserver();
806 this._builder.delete();
807 this._pbBuilder.delete();
810 QueryInterface: ChromeUtils.generateQI([
816 name: "WinTaskbarJumpList",
818 notify: function WTBJL_notify() {
819 // Add idle observer on the first notification so it doesn't hit startup.
820 this._updateIdleObserver();
821 Services.tm.idleDispatchToMainThread(() => {
826 observe: function WTBJL_observe(aSubject, aTopic) {
828 case "nsPref:changed":
829 if (this._enabled && !lazy._prefs.getBoolPref(PREF_TASKBAR_ENABLED)) {
830 this._deleteActiveJumpList();
832 this._refreshPrefs();
834 this._updateIdleObserver();
835 Services.tm.idleDispatchToMainThread(() => {
840 case "profile-before-change":
844 case "browser:purge-session-history":
849 this._timer.cancel();