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
6const { XPCOMUtils } = ChromeUtils.import(
7  "resource://gre/modules/XPCOMUtils.jsm"
8);
9const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
10
11// Stop updating jumplists after some idle time.
12const IDLE_TIMEOUT_SECONDS = 5 * 60;
13
14// Prefs
15const PREF_TASKBAR_BRANCH = "browser.taskbar.lists.";
16const PREF_TASKBAR_ENABLED = "enabled";
17const PREF_TASKBAR_ITEMCOUNT = "maxListItemCount";
18const PREF_TASKBAR_FREQUENT = "frequent.enabled";
19const PREF_TASKBAR_RECENT = "recent.enabled";
20const PREF_TASKBAR_TASKS = "tasks.enabled";
21const PREF_TASKBAR_REFRESH = "refreshInSeconds";
22
23// Hash keys for pendingStatements.
24const LIST_TYPE = {
25  FREQUENT: 0,
26  RECENT: 1,
27};
28
29/**
30 * Exports
31 */
32
33var EXPORTED_SYMBOLS = ["WinTaskbarJumpList"];
34
35/**
36 * Smart getters
37 */
38
39XPCOMUtils.defineLazyGetter(this, "_prefs", function() {
40  return Services.prefs.getBranch(PREF_TASKBAR_BRANCH);
41});
42
43XPCOMUtils.defineLazyGetter(this, "_stringBundle", function() {
44  return Services.strings.createBundle(
45    "chrome://browser/locale/taskbar.properties"
46  );
47});
48
49XPCOMUtils.defineLazyServiceGetter(
50  this,
51  "_idle",
52  "@mozilla.org/widget/useridleservice;1",
53  "nsIUserIdleService"
54);
55XPCOMUtils.defineLazyServiceGetter(
56  this,
57  "_taskbarService",
58  "@mozilla.org/windows-taskbar;1",
59  "nsIWinTaskbar"
60);
61
62ChromeUtils.defineModuleGetter(
63  this,
64  "PlacesUtils",
65  "resource://gre/modules/PlacesUtils.jsm"
66);
67ChromeUtils.defineModuleGetter(
68  this,
69  "PrivateBrowsingUtils",
70  "resource://gre/modules/PrivateBrowsingUtils.jsm"
71);
72
73/**
74 * Global functions
75 */
76
77function _getString(name) {
78  return _stringBundle.GetStringFromName(name);
79}
80
81// Task list configuration data object.
82
83var tasksCfg = [
84  /**
85   * Task configuration options: title, description, args, iconIndex, open, close.
86   *
87   * title       - Task title displayed in the list. (strings in the table are temp fillers.)
88   * description - Tooltip description on the list item.
89   * args        - Command line args to invoke the task.
90   * iconIndex   - Optional win icon index into the main application for the
91   *               list item.
92   * open        - Boolean indicates if the command should be visible after the browser opens.
93   * close       - Boolean indicates if the command should be visible after the browser closes.
94   */
95  // Open new tab
96  {
97    get title() {
98      return _getString("taskbar.tasks.newTab.label");
99    },
100    get description() {
101      return _getString("taskbar.tasks.newTab.description");
102    },
103    args: "-new-tab about:blank",
104    iconIndex: 3, // New window icon
105    open: true,
106    close: true, // The jump list already has an app launch icon, but
107    // we don't always update the list on shutdown.
108    // Thus true for consistency.
109  },
110
111  // Open new window
112  {
113    get title() {
114      return _getString("taskbar.tasks.newWindow.label");
115    },
116    get description() {
117      return _getString("taskbar.tasks.newWindow.description");
118    },
119    args: "-browser",
120    iconIndex: 2, // New tab icon
121    open: true,
122    close: true, // No point, but we don't always update the list on
123    // shutdown. Thus true for consistency.
124  },
125];
126
127// Open new private window
128let privateWindowTask = {
129  get title() {
130    return _getString("taskbar.tasks.newPrivateWindow.label");
131  },
132  get description() {
133    return _getString("taskbar.tasks.newPrivateWindow.description");
134  },
135  args: "-private-window",
136  iconIndex: 4, // Private browsing mode icon
137  open: true,
138  close: true, // No point, but we don't always update the list on
139  // shutdown. Thus true for consistency.
140};
141
142// Implementation
143
144var WinTaskbarJumpList = {
145  _builder: null,
146  _tasks: null,
147  _shuttingDown: false,
148
149  /**
150   * Startup, shutdown, and update
151   */
152
153  startup: function WTBJL_startup() {
154    // exit if this isn't win7 or higher.
155    if (!this._initTaskbar()) {
156      return;
157    }
158
159    // Store our task list config data
160    this._tasks = tasksCfg;
161
162    if (PrivateBrowsingUtils.enabled) {
163      tasksCfg.push(privateWindowTask);
164    }
165
166    // retrieve taskbar related prefs.
167    this._refreshPrefs();
168
169    // observer for private browsing and our prefs branch
170    this._initObs();
171
172    // jump list refresh timer
173    this._updateTimer();
174  },
175
176  update: function WTBJL_update() {
177    // are we disabled via prefs? don't do anything!
178    if (!this._enabled) {
179      return;
180    }
181
182    // do what we came here to do, update the taskbar jumplist
183    this._buildList();
184  },
185
186  _shutdown: function WTBJL__shutdown() {
187    this._shuttingDown = true;
188    this._free();
189  },
190
191  /**
192   * List building
193   *
194   * @note Async builders must add their mozIStoragePendingStatement to
195   *       _pendingStatements object, using a different LIST_TYPE entry for
196   *       each statement. Once finished they must remove it and call
197   *       commitBuild().  When there will be no more _pendingStatements,
198   *       commitBuild() will commit for real.
199   */
200
201  _pendingStatements: {},
202  _hasPendingStatements: function WTBJL__hasPendingStatements() {
203    return !!Object.keys(this._pendingStatements).length;
204  },
205
206  async _buildList() {
207    if (this._hasPendingStatements()) {
208      // We were requested to update the list while another update was in
209      // progress, this could happen at shutdown, idle or privatebrowsing.
210      // Abort the current list building.
211      for (let listType in this._pendingStatements) {
212        this._pendingStatements[listType].cancel();
213        delete this._pendingStatements[listType];
214      }
215      this._builder.abortListBuild();
216    }
217
218    // anything to build?
219    if (!this._showFrequent && !this._showRecent && !this._showTasks) {
220      // don't leave the last list hanging on the taskbar.
221      this._deleteActiveJumpList();
222      return;
223    }
224
225    await this._startBuild();
226
227    if (this._showTasks) {
228      this._buildTasks();
229    }
230
231    // Space for frequent items takes priority over recent.
232    if (this._showFrequent) {
233      this._buildFrequent();
234    }
235
236    if (this._showRecent) {
237      this._buildRecent();
238    }
239
240    this._commitBuild();
241  },
242
243  /**
244   * Taskbar api wrappers
245   */
246
247  async _startBuild() {
248    this._builder.abortListBuild();
249    let URIsToRemove = await this._builder.initListBuild();
250    if (URIsToRemove.length) {
251      // Prior to building, delete removed items from history.
252      this._clearHistory(URIsToRemove);
253    }
254  },
255
256  _commitBuild: function WTBJL__commitBuild() {
257    if (this._hasPendingStatements()) {
258      return;
259    }
260
261    this._builder.commitListBuild(succeed => {
262      if (!succeed) {
263        this._builder.abortListBuild();
264      }
265    });
266  },
267
268  _buildTasks: function WTBJL__buildTasks() {
269    var items = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
270    this._tasks.forEach(function(task) {
271      if (
272        (this._shuttingDown && !task.close) ||
273        (!this._shuttingDown && !task.open)
274      ) {
275        return;
276      }
277      var item = this._getHandlerAppItem(
278        task.title,
279        task.description,
280        task.args,
281        task.iconIndex,
282        null
283      );
284      items.appendElement(item);
285    }, this);
286
287    if (items.length) {
288      this._builder.addListToBuild(
289        this._builder.JUMPLIST_CATEGORY_TASKS,
290        items
291      );
292    }
293  },
294
295  _buildCustom: function WTBJL__buildCustom(title, items) {
296    if (items.length) {
297      this._builder.addListToBuild(
298        this._builder.JUMPLIST_CATEGORY_CUSTOMLIST,
299        items,
300        title
301      );
302    }
303  },
304
305  _buildFrequent: function WTBJL__buildFrequent() {
306    // Windows supports default frequent and recent lists,
307    // but those depend on internal windows visit tracking
308    // which we don't populate. So we build our own custom
309    // frequent and recent lists using our nav history data.
310
311    var items = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
312    // track frequent items so that we don't add them to
313    // the recent list.
314    this._frequentHashList = [];
315
316    this._pendingStatements[LIST_TYPE.FREQUENT] = this._getHistoryResults(
317      Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING,
318      this._maxItemCount,
319      function(aResult) {
320        if (!aResult) {
321          delete this._pendingStatements[LIST_TYPE.FREQUENT];
322          // The are no more results, build the list.
323          this._buildCustom(_getString("taskbar.frequent.label"), items);
324          this._commitBuild();
325          return;
326        }
327
328        let title = aResult.title || aResult.uri;
329        let faviconPageUri = Services.io.newURI(aResult.uri);
330        let shortcut = this._getHandlerAppItem(
331          title,
332          title,
333          aResult.uri,
334          1,
335          faviconPageUri
336        );
337        items.appendElement(shortcut);
338        this._frequentHashList.push(aResult.uri);
339      },
340      this
341    );
342  },
343
344  _buildRecent: function WTBJL__buildRecent() {
345    var items = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
346    // Frequent items will be skipped, so we select a double amount of
347    // entries and stop fetching results at _maxItemCount.
348    var count = 0;
349
350    this._pendingStatements[LIST_TYPE.RECENT] = this._getHistoryResults(
351      Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING,
352      this._maxItemCount * 2,
353      function(aResult) {
354        if (!aResult) {
355          // The are no more results, build the list.
356          this._buildCustom(_getString("taskbar.recent.label"), items);
357          delete this._pendingStatements[LIST_TYPE.RECENT];
358          this._commitBuild();
359          return;
360        }
361
362        if (count >= this._maxItemCount) {
363          return;
364        }
365
366        // Do not add items to recent that have already been added to frequent.
367        if (
368          this._frequentHashList &&
369          this._frequentHashList.includes(aResult.uri)
370        ) {
371          return;
372        }
373
374        let title = aResult.title || aResult.uri;
375        let faviconPageUri = Services.io.newURI(aResult.uri);
376        let shortcut = this._getHandlerAppItem(
377          title,
378          title,
379          aResult.uri,
380          1,
381          faviconPageUri
382        );
383        items.appendElement(shortcut);
384        count++;
385      },
386      this
387    );
388  },
389
390  _deleteActiveJumpList: function WTBJL__deleteAJL() {
391    this._builder.deleteActiveList();
392  },
393
394  /**
395   * Jump list item creation helpers
396   */
397
398  _getHandlerAppItem: function WTBJL__getHandlerAppItem(
399    name,
400    description,
401    args,
402    iconIndex,
403    faviconPageUri
404  ) {
405    var file = Services.dirsvc.get("XREExeF", Ci.nsIFile);
406
407    var handlerApp = Cc[
408      "@mozilla.org/uriloader/local-handler-app;1"
409    ].createInstance(Ci.nsILocalHandlerApp);
410    handlerApp.executable = file;
411    // handlers default to the leaf name if a name is not specified
412    if (name && name.length) {
413      handlerApp.name = name;
414    }
415    handlerApp.detailedDescription = description;
416    handlerApp.appendParameter(args);
417
418    var item = Cc["@mozilla.org/windows-jumplistshortcut;1"].createInstance(
419      Ci.nsIJumpListShortcut
420    );
421    item.app = handlerApp;
422    item.iconIndex = iconIndex;
423    item.faviconPageUri = faviconPageUri;
424    return item;
425  },
426
427  _getSeparatorItem: function WTBJL__getSeparatorItem() {
428    var item = Cc["@mozilla.org/windows-jumplistseparator;1"].createInstance(
429      Ci.nsIJumpListSeparator
430    );
431    return item;
432  },
433
434  /**
435   * Nav history helpers
436   */
437
438  _getHistoryResults: function WTBLJL__getHistoryResults(
439    aSortingMode,
440    aLimit,
441    aCallback,
442    aScope
443  ) {
444    var options = PlacesUtils.history.getNewQueryOptions();
445    options.maxResults = aLimit;
446    options.sortingMode = aSortingMode;
447    var query = PlacesUtils.history.getNewQuery();
448
449    // Return the pending statement to the caller, to allow cancelation.
450    return PlacesUtils.history.asyncExecuteLegacyQuery(query, options, {
451      handleResult(aResultSet) {
452        for (let row; (row = aResultSet.getNextRow()); ) {
453          try {
454            aCallback.call(aScope, {
455              uri: row.getResultByIndex(1),
456              title: row.getResultByIndex(2),
457            });
458          } catch (e) {}
459        }
460      },
461      handleError(aError) {
462        Cu.reportError(
463          "Async execution error (" + aError.result + "): " + aError.message
464        );
465      },
466      handleCompletion(aReason) {
467        aCallback.call(WinTaskbarJumpList, null);
468      },
469    });
470  },
471
472  _clearHistory: function WTBJL__clearHistory(uriSpecsToRemove) {
473    let URIsToRemove = uriSpecsToRemove
474      .map(spec => {
475        try {
476          // in case we get a bad uri
477          return Services.io.newURI(spec);
478        } catch (e) {
479          return null;
480        }
481      })
482      .filter(uri => !!uri);
483
484    if (URIsToRemove.length) {
485      PlacesUtils.history.remove(URIsToRemove).catch(Cu.reportError);
486    }
487  },
488
489  /**
490   * Prefs utilities
491   */
492
493  _refreshPrefs: function WTBJL__refreshPrefs() {
494    this._enabled = _prefs.getBoolPref(PREF_TASKBAR_ENABLED);
495    this._showFrequent = _prefs.getBoolPref(PREF_TASKBAR_FREQUENT);
496    this._showRecent = _prefs.getBoolPref(PREF_TASKBAR_RECENT);
497    this._showTasks = _prefs.getBoolPref(PREF_TASKBAR_TASKS);
498    this._maxItemCount = _prefs.getIntPref(PREF_TASKBAR_ITEMCOUNT);
499  },
500
501  /**
502   * Init and shutdown utilities
503   */
504
505  _initTaskbar: function WTBJL__initTaskbar() {
506    this._builder = _taskbarService.createJumpListBuilder();
507    if (!this._builder || !this._builder.available) {
508      return false;
509    }
510
511    return true;
512  },
513
514  _initObs: function WTBJL__initObs() {
515    // If the browser is closed while in private browsing mode, the "exit"
516    // notification is fired on quit-application-granted.
517    // History cleanup can happen at profile-change-teardown.
518    Services.obs.addObserver(this, "profile-before-change");
519    Services.obs.addObserver(this, "browser:purge-session-history");
520    _prefs.addObserver("", this);
521    this._placesObserver = new PlacesWeakCallbackWrapper(
522      this.update.bind(this)
523    );
524    PlacesUtils.observers.addListener(
525      ["history-cleared"],
526      this._placesObserver
527    );
528  },
529
530  _freeObs: function WTBJL__freeObs() {
531    Services.obs.removeObserver(this, "profile-before-change");
532    Services.obs.removeObserver(this, "browser:purge-session-history");
533    _prefs.removeObserver("", this);
534    if (this._placesObserver) {
535      PlacesUtils.observers.removeListener(
536        ["history-cleared"],
537        this._placesObserver
538      );
539    }
540  },
541
542  _updateTimer: function WTBJL__updateTimer() {
543    if (this._enabled && !this._shuttingDown && !this._timer) {
544      this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
545      this._timer.initWithCallback(
546        this,
547        _prefs.getIntPref(PREF_TASKBAR_REFRESH) * 1000,
548        this._timer.TYPE_REPEATING_SLACK
549      );
550    } else if ((!this._enabled || this._shuttingDown) && this._timer) {
551      this._timer.cancel();
552      delete this._timer;
553    }
554  },
555
556  _hasIdleObserver: false,
557  _updateIdleObserver: function WTBJL__updateIdleObserver() {
558    if (this._enabled && !this._shuttingDown && !this._hasIdleObserver) {
559      _idle.addIdleObserver(this, IDLE_TIMEOUT_SECONDS);
560      this._hasIdleObserver = true;
561    } else if (
562      (!this._enabled || this._shuttingDown) &&
563      this._hasIdleObserver
564    ) {
565      _idle.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS);
566      this._hasIdleObserver = false;
567    }
568  },
569
570  _free: function WTBJL__free() {
571    this._freeObs();
572    this._updateTimer();
573    this._updateIdleObserver();
574    delete this._builder;
575  },
576
577  notify: function WTBJL_notify(aTimer) {
578    // Add idle observer on the first notification so it doesn't hit startup.
579    this._updateIdleObserver();
580    Services.tm.idleDispatchToMainThread(() => {
581      this.update();
582    });
583  },
584
585  observe: function WTBJL_observe(aSubject, aTopic, aData) {
586    switch (aTopic) {
587      case "nsPref:changed":
588        if (this._enabled && !_prefs.getBoolPref(PREF_TASKBAR_ENABLED)) {
589          this._deleteActiveJumpList();
590        }
591        this._refreshPrefs();
592        this._updateTimer();
593        this._updateIdleObserver();
594        Services.tm.idleDispatchToMainThread(() => {
595          this.update();
596        });
597        break;
598
599      case "profile-before-change":
600        this._shutdown();
601        break;
602
603      case "browser:purge-session-history":
604        this.update();
605        break;
606      case "idle":
607        if (this._timer) {
608          this._timer.cancel();
609          delete this._timer;
610        }
611        break;
612
613      case "active":
614        this._updateTimer();
615        break;
616    }
617  },
618};
619