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