summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/activity
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/activity')
-rw-r--r--comm/mail/components/activity/Activity.jsm322
-rw-r--r--comm/mail/components/activity/ActivityManager.jsm157
-rw-r--r--comm/mail/components/activity/ActivityManagerUI.jsm47
-rw-r--r--comm/mail/components/activity/components.conf38
-rw-r--r--comm/mail/components/activity/content/activity-widgets.js384
-rw-r--r--comm/mail/components/activity/content/activity.js239
-rw-r--r--comm/mail/components/activity/content/activity.xhtml61
-rw-r--r--comm/mail/components/activity/jar.mn8
-rw-r--r--comm/mail/components/activity/modules/activityModules.jsm33
-rw-r--r--comm/mail/components/activity/modules/alertHook.jsm101
-rw-r--r--comm/mail/components/activity/modules/autosync.jsm433
-rw-r--r--comm/mail/components/activity/modules/glodaIndexer.jsm251
-rw-r--r--comm/mail/components/activity/modules/moveCopy.jsm396
-rw-r--r--comm/mail/components/activity/modules/pop3Download.jsm154
-rw-r--r--comm/mail/components/activity/modules/sendLater.jsm298
-rw-r--r--comm/mail/components/activity/moz.build34
-rw-r--r--comm/mail/components/activity/nsIActivity.idl492
-rw-r--r--comm/mail/components/activity/nsIActivityManager.idl135
-rw-r--r--comm/mail/components/activity/nsIActivityManagerUI.idl50
19 files changed, 3633 insertions, 0 deletions
diff --git a/comm/mail/components/activity/Activity.jsm b/comm/mail/components/activity/Activity.jsm
new file mode 100644
index 0000000000..1b23efe1c7
--- /dev/null
+++ b/comm/mail/components/activity/Activity.jsm
@@ -0,0 +1,322 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ["ActivityProcess", "ActivityEvent", "ActivityWarning"];
+
+// Base class for ActivityProcess and ActivityEvent objects
+
+function Activity() {
+ this._initLogging();
+ this._listeners = [];
+ this._subjects = [];
+}
+
+Activity.prototype = {
+ id: -1,
+ bindingName: "",
+ iconClass: "",
+ groupingStyle: Ci.nsIActivity.GROUPING_STYLE_BYCONTEXT,
+ facet: "",
+ displayText: "",
+ initiator: null,
+ contextType: "",
+ context: "",
+ contextObj: null,
+
+ _initLogging() {
+ this.log = console.createInstance({
+ prefix: "mail.activity",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.activity.loglevel",
+ });
+ },
+
+ addListener(aListener) {
+ this._listeners.push(aListener);
+ },
+
+ removeListener(aListener) {
+ for (let i = 0; i < this._listeners.length; i++) {
+ if (this._listeners[i] == aListener) {
+ this._listeners.splice(i, 1);
+ break;
+ }
+ }
+ },
+
+ addSubject(aSubject) {
+ this._subjects.push(aSubject);
+ },
+
+ getSubjects() {
+ return this._subjects.slice();
+ },
+};
+
+function ActivityProcess() {
+ Activity.call(this);
+ this.bindingName = "activity-process-item";
+ this.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_BYCONTEXT;
+}
+
+ActivityProcess.prototype = {
+ __proto__: Activity.prototype,
+
+ percentComplete: -1,
+ lastStatusText: "",
+ workUnitComplete: 0,
+ totalWorkUnits: 0,
+ startTime: Date.now(),
+ _cancelHandler: null,
+ _pauseHandler: null,
+ _retryHandler: null,
+ _state: Ci.nsIActivityProcess.STATE_INPROGRESS,
+
+ init(aDisplayText, aInitiator) {
+ this.displayText = aDisplayText;
+ this.initiator = aInitiator;
+ },
+
+ get state() {
+ return this._state;
+ },
+
+ set state(val) {
+ if (val == this._state) {
+ return;
+ }
+
+ // test validity of the new state
+ //
+ if (
+ this._state == Ci.nsIActivityProcess.STATE_INPROGRESS &&
+ !(
+ val == Ci.nsIActivityProcess.STATE_COMPLETED ||
+ val == Ci.nsIActivityProcess.STATE_CANCELED ||
+ val == Ci.nsIActivityProcess.STATE_WAITINGFORRETRY ||
+ val == Ci.nsIActivityProcess.STATE_WAITINGFORINPUT ||
+ val == Ci.nsIActivityProcess.STATE_PAUSED
+ )
+ ) {
+ throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+
+ // we cannot change the state after the activity is completed,
+ // or it is canceled.
+ if (
+ this._state == Ci.nsIActivityProcess.STATE_COMPLETED ||
+ this._state == Ci.nsIActivityProcess.STATE_CANCELED
+ ) {
+ throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+
+ if (
+ this._state == Ci.nsIActivityProcess.STATE_PAUSED &&
+ !(
+ val == Ci.nsIActivityProcess.STATE_COMPLETED ||
+ val == Ci.nsIActivityProcess.STATE_INPROGRESS ||
+ val == Ci.nsIActivityProcess.STATE_WAITINGFORRETRY ||
+ val == Ci.nsIActivityProcess.STATE_WAITINGFORINPUT ||
+ val == Ci.nsIActivityProcess.STATE_CANCELED
+ )
+ ) {
+ throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+
+ if (
+ this._state == Ci.nsIActivityProcess.STATE_WAITINGFORINPUT &&
+ !(
+ val == Ci.nsIActivityProcess.STATE_INPROGRESS ||
+ val == Ci.nsIActivityProcess.STATE_CANCELED
+ )
+ ) {
+ throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+
+ if (
+ this._state == Ci.nsIActivityProcess.STATE_WAITINGFORRETRY &&
+ !(
+ val == Ci.nsIActivityProcess.STATE_INPROGRESS ||
+ val == Ci.nsIActivityProcess.STATE_CANCELED
+ )
+ ) {
+ throw Components.Exception("", Cr.NS_ERROR_ILLEGAL_VALUE);
+ }
+
+ let oldState = this._state;
+ this._state = val;
+
+ // let the listeners know about the change
+ this.log.debug("Notifying onStateChanged listeners");
+ for (let value of this._listeners) {
+ try {
+ value.onStateChanged(this, oldState);
+ } catch (e) {
+ this.log.error("Exception thrown by onStateChanged listener: " + e);
+ }
+ }
+ },
+
+ setProgress(aStatusText, aWorkUnitsComplete, aTotalWorkUnits) {
+ if (aTotalWorkUnits == 0) {
+ this.percentComplete = -1;
+ this.workUnitComplete = 0;
+ this.totalWorkUnits = 0;
+ } else {
+ this.percentComplete = parseInt(
+ (100.0 * aWorkUnitsComplete) / aTotalWorkUnits
+ );
+ this.workUnitComplete = aWorkUnitsComplete;
+ this.totalWorkUnits = aTotalWorkUnits;
+ }
+ this.lastStatusText = aStatusText;
+
+ // notify listeners
+ for (let value of this._listeners) {
+ try {
+ value.onProgressChanged(
+ this,
+ aStatusText,
+ aWorkUnitsComplete,
+ aTotalWorkUnits
+ );
+ } catch (e) {
+ this.log.error("Exception thrown by onProgressChanged listener: " + e);
+ }
+ }
+ },
+
+ get cancelHandler() {
+ return this._cancelHandler;
+ },
+
+ set cancelHandler(val) {
+ this._cancelHandler = val;
+
+ // let the listeners know about the change
+ this.log.debug("Notifying onHandlerChanged listeners");
+ for (let value of this._listeners) {
+ try {
+ value.onHandlerChanged(this);
+ } catch (e) {
+ this.log.error("Exception thrown by onHandlerChanged listener: " + e);
+ }
+ }
+ },
+
+ get pauseHandler() {
+ return this._pauseHandler;
+ },
+
+ set pauseHandler(val) {
+ this._pauseHandler = val;
+
+ // let the listeners know about the change
+ this.log.debug("Notifying onHandlerChanged listeners");
+ for (let value of this._listeners) {
+ value.onHandlerChanged(this);
+ }
+ },
+
+ get retryHandler() {
+ return this._retryHandler;
+ },
+
+ set retryHandler(val) {
+ this._retryHandler = val;
+
+ // let the listeners know about the change
+ this.log.debug("Notifying onHandlerChanged listeners");
+ for (let value of this._listeners) {
+ value.onHandlerChanged(this);
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIActivityProcess", "nsIActivity"]),
+};
+
+function ActivityEvent() {
+ Activity.call(this);
+ this.bindingName = "activity-event-item";
+ this.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_STANDALONE;
+}
+
+ActivityEvent.prototype = {
+ __proto__: Activity.prototype,
+
+ statusText: "",
+ startTime: 0,
+ completionTime: 0,
+ _undoHandler: null,
+
+ init(aDisplayText, aInitiator, aStatusText, aStartTime, aCompletionTime) {
+ this.displayText = aDisplayText;
+ this.statusText = aStatusText;
+ this.startTime = aStartTime;
+ if (aCompletionTime) {
+ this.completionTime = aCompletionTime;
+ } else {
+ this.completionTime = Date.now();
+ }
+ this.initiator = aInitiator;
+ this._completionTime = aCompletionTime;
+ },
+
+ get undoHandler() {
+ return this._undoHandler;
+ },
+
+ set undoHandler(val) {
+ this._undoHandler = val;
+
+ // let the listeners know about the change
+ this.log.debug("Notifying onHandlerChanged listeners");
+ for (let value of this._listeners) {
+ value.onHandlerChanged(this);
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIActivityEvent", "nsIActivity"]),
+};
+
+function ActivityWarning() {
+ Activity.call(this);
+ this.bindingName = "activity-warning-item";
+ this.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_BYCONTEXT;
+}
+
+ActivityWarning.prototype = {
+ __proto__: Activity.prototype,
+
+ recoveryTipText: "",
+ _time: 0,
+ _recoveryHandler: null,
+
+ init(aWarningText, aInitiator, aRecoveryTipText) {
+ this.displayText = aWarningText;
+ this.initiator = aInitiator;
+ this.recoveryTipText = aRecoveryTipText;
+ this._time = Date.now();
+ },
+
+ get recoveryHandler() {
+ return this._recoveryHandler;
+ },
+
+ set recoveryHandler(val) {
+ this._recoveryHandler = val;
+
+ // let the listeners know about the change
+ this.log.debug("Notifying onHandlerChanged listeners");
+ for (let value of this._listeners) {
+ value.onHandlerChanged(this);
+ }
+ },
+
+ get time() {
+ return this._time;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIActivityWarning", "nsIActivity"]),
+};
diff --git a/comm/mail/components/activity/ActivityManager.jsm b/comm/mail/components/activity/ActivityManager.jsm
new file mode 100644
index 0000000000..c808cb3354
--- /dev/null
+++ b/comm/mail/components/activity/ActivityManager.jsm
@@ -0,0 +1,157 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ["ActivityManager"];
+
+function ActivityManager() {}
+
+ActivityManager.prototype = {
+ log: console.createInstance({
+ prefix: "mail.activity",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.activity.loglevel",
+ }),
+ _listeners: [],
+ _processCount: 0,
+ _db: null,
+ _idCounter: 1,
+ _activities: new Map(),
+
+ get processCount() {
+ let count = 0;
+ for (let value of this._activities.values()) {
+ if (value instanceof Ci.nsIActivityProcess) {
+ count++;
+ }
+ }
+
+ return count;
+ },
+
+ getProcessesByContext(aContextType, aContextObj) {
+ let list = [];
+ for (let activity of this._activities.values()) {
+ if (
+ activity instanceof Ci.nsIActivityProcess &&
+ activity.contextType == aContextType &&
+ activity.contextObj == aContextObj
+ ) {
+ list.push(activity);
+ }
+ }
+ return list;
+ },
+
+ get db() {
+ return null;
+ },
+
+ get nextId() {
+ return this._idCounter++;
+ },
+
+ addActivity(aActivity) {
+ try {
+ this.log.info("adding Activity");
+ // get the next valid id for this activity
+ let id = this.nextId;
+ aActivity.id = id;
+
+ // add activity into the activities table
+ this._activities.set(id, aActivity);
+ // notify all the listeners
+ for (let value of this._listeners) {
+ try {
+ value.onAddedActivity(id, aActivity);
+ } catch (e) {
+ this.log.error("Exception calling onAddedActivity" + e);
+ }
+ }
+ return id;
+ } catch (e) {
+ // for some reason exceptions don't end up on the console if we don't
+ // explicitly log them.
+ this.log.error("Exception: " + e);
+ throw e;
+ }
+ },
+
+ removeActivity(aID) {
+ let activity = this.getActivity(aID);
+ if (!activity) {
+ return; // Nothing to remove.
+ }
+
+ // make sure that the activity is not in-progress state
+ if (
+ activity instanceof Ci.nsIActivityProcess &&
+ activity.state == Ci.nsIActivityProcess.STATE_INPROGRESS
+ ) {
+ throw Components.Exception(`Activity in progress`, Cr.NS_ERROR_FAILURE);
+ }
+
+ // remove the activity
+ this._activities.delete(aID);
+
+ // notify all the listeners
+ for (let value of this._listeners) {
+ try {
+ value.onRemovedActivity(aID);
+ } catch (e) {
+ // ignore the exception
+ }
+ }
+ },
+
+ cleanUp() {
+ // Get the list of aIDs.
+ this.log.info("cleanUp\n");
+ for (let [id, activity] of this._activities) {
+ if (activity instanceof Ci.nsIActivityProcess) {
+ // Note: The .state property will return undefined if you aren't in
+ // this if-instanceof block.
+ let state = activity.state;
+ if (
+ state != Ci.nsIActivityProcess.STATE_INPROGRESS &&
+ state != Ci.nsIActivityProcess.STATE_PAUSED &&
+ state != Ci.nsIActivityProcess.STATE_WAITINGFORINPUT &&
+ state != Ci.nsIActivityProcess.STATE_WAITINGFORRETRY
+ ) {
+ this.removeActivity(id);
+ }
+ } else {
+ this.removeActivity(id);
+ }
+ }
+ },
+
+ getActivity(aID) {
+ return this._activities.get(aID);
+ },
+
+ containsActivity(aID) {
+ return this._activities.has(aID);
+ },
+
+ getActivities() {
+ return [...this._activities.values()];
+ },
+
+ addListener(aListener) {
+ this.log.info("addListener\n");
+ this._listeners.push(aListener);
+ },
+
+ removeListener(aListener) {
+ this.log.info("removeListener\n");
+ for (let i = 0; i < this._listeners.length; i++) {
+ if (this._listeners[i] == aListener) {
+ this._listeners.splice(i, 1);
+ }
+ }
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIActivityManager"]),
+};
diff --git a/comm/mail/components/activity/ActivityManagerUI.jsm b/comm/mail/components/activity/ActivityManagerUI.jsm
new file mode 100644
index 0000000000..b36b9b5a72
--- /dev/null
+++ b/comm/mail/components/activity/ActivityManagerUI.jsm
@@ -0,0 +1,47 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var EXPORTED_SYMBOLS = ["ActivityManagerUI"];
+
+const ACTIVITY_MANAGER_URL = "chrome://messenger/content/activity.xhtml";
+
+function ActivityManagerUI() {}
+
+ActivityManagerUI.prototype = {
+ show(aWindowContext, aID) {
+ // First we see if it is already visible
+ let window = this.recentWindow;
+ if (window) {
+ window.focus();
+ return;
+ }
+
+ let parent = null;
+ try {
+ if (aWindowContext) {
+ parent = aWindowContext.docShell.domWindow;
+ }
+ } catch (e) {
+ /* it's OK to not have a parent window */
+ }
+
+ Services.ww.openWindow(
+ parent,
+ ACTIVITY_MANAGER_URL,
+ "ActivityManager",
+ "chrome,dialog=no,resizable",
+ {}
+ );
+ },
+
+ get visible() {
+ return null != this.recentWindow;
+ },
+
+ get recentWindow() {
+ return Services.wm.getMostRecentWindow("Activity:Manager");
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIActivityManagerUI"]),
+};
diff --git a/comm/mail/components/activity/components.conf b/comm/mail/components/activity/components.conf
new file mode 100644
index 0000000000..429fd62bb8
--- /dev/null
+++ b/comm/mail/components/activity/components.conf
@@ -0,0 +1,38 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+ {
+ 'cid': '{B2C036A3-F7CE-401C-95EE-9C21505167FD}',
+ 'contract_ids': ['@mozilla.org/activity-process;1'],
+ 'jsm': 'resource:///modules/Activity.jsm',
+ 'constructor': 'ActivityProcess',
+ },
+ {
+ 'cid': '{87AAEB20-89D9-4B95-9542-3BF72405CAB2}',
+ 'contract_ids': ['@mozilla.org/activity-event;1'],
+ 'jsm': 'resource:///modules/Activity.jsm',
+ 'constructor': 'ActivityEvent',
+ },
+ {
+ 'cid': '{968BAC9E-798B-4952-B384-86B21B8CC71E}',
+ 'contract_ids': ['@mozilla.org/activity-warning;1'],
+ 'jsm': 'resource:///modules/Activity.jsm',
+ 'constructor': 'ActivityWarning',
+ },
+ {
+ 'cid': '{8aa5972e-19cb-41cc-9696-645f8a8d1a06}',
+ 'contract_ids': ['@mozilla.org/activity-manager;1'],
+ 'jsm': 'resource:///modules/ActivityManager.jsm',
+ 'constructor': 'ActivityManager',
+ },
+ {
+ 'cid': '{5fa5974e-09cb-40cc-9696-643f8a8d9a06}',
+ 'contract_ids': ['@mozilla.org/activity-manager-ui;1'],
+ 'jsm': 'resource:///modules/ActivityManagerUI.jsm',
+ 'constructor': 'ActivityManagerUI',
+ },
+]
diff --git a/comm/mail/components/activity/content/activity-widgets.js b/comm/mail/components/activity/content/activity-widgets.js
new file mode 100644
index 0000000000..44ee16bff8
--- /dev/null
+++ b/comm/mail/components/activity/content/activity-widgets.js
@@ -0,0 +1,384 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/* global MozXULElement, activityManager */
+
+// Wrap in a block to prevent leaking to window scope.
+{
+ const { makeFriendlyDateAgo } = ChromeUtils.import(
+ "resource:///modules/TemplateUtils.jsm"
+ );
+
+ let activityStrings = Services.strings.createBundle(
+ "chrome://messenger/locale/activity.properties"
+ );
+
+ /**
+ * The ActivityItemBase widget is the base class for all the activity item.
+ * It initializes activity details: i.e. id, status, icon, name, progress,
+ * date etc. for the activity widgets.
+ *
+ * @abstract
+ * @augments HTMLLIElement
+ */
+ class ActivityItemBase extends HTMLLIElement {
+ connectedCallback() {
+ if (!this.hasChildNodes()) {
+ // fetch the activity and set the base attributes
+ this.log = console.createInstance({
+ prefix: "mail.activity",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.activity.loglevel",
+ });
+ let actID = this.getAttribute("actID");
+ this._activity = activityManager.getActivity(actID);
+ this._activity.QueryInterface(this.constructor.activityInterface);
+
+ // Construct the children.
+ this.classList.add("activityitem");
+
+ let icon = document.createElement("img");
+ icon.setAttribute(
+ "src",
+ this._activity.iconClass
+ ? `chrome://messenger/skin/icons/new/activity/${this._activity.iconClass}Icon.svg`
+ : this.constructor.defaultIconSrc
+ );
+ icon.setAttribute("alt", "");
+ this.appendChild(icon);
+
+ let display = document.createElement("span");
+ display.classList.add("displayText");
+ this.appendChild(display);
+
+ if (this.isEvent || this.isWarning) {
+ let time = document.createElement("time");
+ time.classList.add("dateTime");
+ this.appendChild(time);
+ }
+
+ if (this.isProcess) {
+ let progress = document.createElement("progress");
+ progress.setAttribute("value", "0");
+ progress.setAttribute("max", "100");
+ progress.classList.add("progressmeter");
+ this.appendChild(progress);
+ }
+
+ let statusText = document.createElement("span");
+ statusText.setAttribute("role", "note");
+ statusText.classList.add("statusText");
+ this.appendChild(statusText);
+ }
+ // (Re-)Attach the listener.
+ this.attachToActivity();
+ }
+
+ disconnectedCallback() {
+ this.detachFromActivity();
+ }
+
+ get isProcess() {
+ return this.constructor.activityInterface == Ci.nsIActivityProcess;
+ }
+
+ get isEvent() {
+ return this.constructor.activityInterface == Ci.nsIActivityEvent;
+ }
+
+ get isWarning() {
+ return this.constructor.activityInterface == Ci.nsIActivityWarning;
+ }
+
+ get isGroup() {
+ return false;
+ }
+
+ get activity() {
+ return this._activity;
+ }
+
+ detachFromActivity() {
+ if (this.activityListener) {
+ this._activity.removeListener(this.activityListener);
+ }
+ }
+
+ attachToActivity() {
+ if (this.activityListener) {
+ this._activity.addListener(this.activityListener);
+ }
+ }
+
+ static _dateTimeFormatter = new Services.intl.DateTimeFormat(undefined, {
+ dateStyle: "long",
+ timeStyle: "short",
+ });
+
+ /**
+ * The time the activity occurred.
+ *
+ * @type {number} - The time in milliseconds since the epoch.
+ */
+ set dateTime(time) {
+ let element = this.querySelector(".dateTime");
+ if (!element) {
+ return;
+ }
+ time = new Date(parseInt(time));
+
+ element.setAttribute("datetime", time.toISOString());
+ element.textContent = makeFriendlyDateAgo(time);
+ element.setAttribute(
+ "title",
+ this.constructor._dateTimeFormatter.format(time)
+ );
+ }
+
+ /**
+ * The text that describes additional information to the user.
+ *
+ * @type {string}
+ */
+ set statusText(val) {
+ this.querySelector(".statusText").textContent = val;
+ }
+
+ get statusText() {
+ return this.querySelector(".statusText").textContent;
+ }
+
+ /**
+ * The text that describes the activity to the user.
+ *
+ * @type {string}
+ */
+ set displayText(val) {
+ this.querySelector(".displayText").textContent = val;
+ }
+
+ get displayText() {
+ return this.querySelector(".displayText").textContent;
+ }
+ }
+
+ /**
+ * The MozActivityEvent widget displays information about events (like
+ * deleting or moving the message): e.g image, name, date and description.
+ * It is typically used in Activity Manager window.
+ *
+ * @augments ActivityItemBase
+ */
+ class ActivityEventItem extends ActivityItemBase {
+ static defaultIconSrc =
+ "chrome://messenger/skin/icons/new/activity/defaultEventIcon.svg";
+ static activityInterface = Ci.nsIActivityEvent;
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.setAttribute("is", "activity-event-item");
+
+ this.displayText = this.activity.displayText;
+ this.statusText = this.activity.statusText;
+ this.dateTime = this.activity.completionTime;
+ }
+ }
+
+ customElements.define("activity-event-item", ActivityEventItem, {
+ extends: "li",
+ });
+
+ /**
+ * The ActivityGroupItem widget displays information about the activities of
+ * the group: e.g. name of the group, list of the activities with their name,
+ * progress and icon. It is shown in Activity Manager window. It gets removed
+ * when there is no activities from the group.
+ *
+ * @augments HTMLLIElement
+ */
+ class ActivityGroupItem extends HTMLLIElement {
+ constructor() {
+ super();
+
+ let heading = document.createElement("h2");
+ heading.classList.add("contextDisplayText");
+ this.appendChild(heading);
+
+ let list = document.createElement("ul");
+ list.classList.add("activitygroup-list", "activityview");
+ this.appendChild(list);
+
+ this.classList.add("activitygroup");
+ this.setAttribute("is", "activity-group-item");
+ }
+
+ /**
+ * The text heading for the group, as seen by the user.
+ *
+ * @type {string}
+ */
+ set contextDisplayText(val) {
+ this.querySelector(".contextDisplayText").textContent = val;
+ }
+
+ get contextDisplayText() {
+ return this.querySelctor(".contextDisplayText").textContent;
+ }
+
+ get isGroup() {
+ return true;
+ }
+ }
+
+ customElements.define("activity-group-item", ActivityGroupItem, {
+ extends: "li",
+ });
+
+ /**
+ * The ActivityProcessItem widget displays information about the internal
+ * process : e.g image, progress, name, date and description.
+ * It is typically used in Activity Manager window.
+ *
+ * @augments ActivityItemBase
+ */
+ class ActivityProcessItem extends ActivityItemBase {
+ static defaultIconSrc =
+ "chrome://messenger/skin/icons/new/activity/deafultProcessIcon.svg";
+ static activityInterface = Ci.nsIActivityProcess;
+ static textMap = {
+ paused: activityStrings.GetStringFromName("paused2"),
+ canceled: activityStrings.GetStringFromName("canceled"),
+ failed: activityStrings.GetStringFromName("failed"),
+ waitingforinput: activityStrings.GetStringFromName("waitingForInput"),
+ waitingforretry: activityStrings.GetStringFromName("waitingForRetry"),
+ };
+
+ constructor() {
+ super();
+
+ this.activityListener = {
+ onStateChanged: (activity, oldState) => {
+ // change the view of the element according to the new state
+ // default states for each item
+ let hideProgressMeter = false;
+ let statusText = this.statusText;
+
+ switch (this.activity.state) {
+ case Ci.nsIActivityProcess.STATE_INPROGRESS:
+ statusText = "";
+ break;
+ case Ci.nsIActivityProcess.STATE_COMPLETED:
+ hideProgressMeter = true;
+ statusText = "";
+ break;
+ case Ci.nsIActivityProcess.STATE_CANCELED:
+ hideProgressMeter = true;
+ statusText = this.constructor.textMap.canceled;
+ break;
+ case Ci.nsIActivityProcess.STATE_PAUSED:
+ statusText = this.constructor.textMap.paused;
+ break;
+ case Ci.nsIActivityProcess.STATE_WAITINGFORINPUT:
+ statusText = this.constructor.textMap.waitingforinput;
+ break;
+ case Ci.nsIActivityProcess.STATE_WAITINGFORRETRY:
+ hideProgressMeter = true;
+ statusText = this.constructor.textMap.waitingforretry;
+ break;
+ }
+
+ // Set the visibility
+ let meter = this.querySelector(".progressmeter");
+ meter.hidden = hideProgressMeter;
+
+ // Ensure progress meter not active when hidden
+ if (hideProgressMeter) {
+ meter.value = 0;
+ }
+
+ // Update Status text and Display Text Areas
+ // In some states we need to modify Display Text area of
+ // the process (e.g. Failure).
+ this.statusText = statusText;
+ },
+ onProgressChanged: (
+ activity,
+ statusText,
+ workUnitsComplete,
+ totalWorkUnits
+ ) => {
+ let element = document.querySelector(".progressmeter");
+ if (totalWorkUnits == 0) {
+ element.removeAttribute("value");
+ } else {
+ let _percentComplete = (100.0 * workUnitsComplete) / totalWorkUnits;
+ element.value = _percentComplete;
+ }
+ this.statusText = statusText;
+ },
+ };
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.setAttribute("is", "activity-process-item");
+
+ this.displayText = this.activity.displayText;
+ // make sure that custom element reflects the latest state of the process
+ this.activityListener.onStateChanged(
+ this.activity.state,
+ Ci.nsIActivityProcess.STATE_NOTSTARTED
+ );
+ this.activityListener.onProgressChanged(
+ this.activity,
+ this.activity.lastStatusText,
+ this.activity.workUnitComplete,
+ this.activity.totalWorkUnits
+ );
+ }
+
+ get inProgress() {
+ return this.activity.state == Ci.nsIActivityProcess.STATE_INPROGRESS;
+ }
+
+ get isRemovable() {
+ return (
+ this.activity.state == Ci.nsIActivityProcess.STATE_COMPLETED ||
+ this.activity.state == Ci.nsIActivityProcess.STATE_CANCELED
+ );
+ }
+ }
+
+ customElements.define("activity-process-item", ActivityProcessItem, {
+ extends: "li",
+ });
+
+ /**
+ * The ActivityWarningItem widget displays information about
+ * warnings : e.g image, name, date and description.
+ * It is typically used in Activity Manager window.
+ *
+ * @augments ActivityItemBase
+ */
+ class ActivityWarningItem extends ActivityItemBase {
+ static defaultIconSrc =
+ "chrome://messenger/skin/icons/new/activity/warning.svg";
+ static activityInterface = Ci.nsIActivityWarning;
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.setAttribute("is", "activity-warning-item");
+
+ this.displayText = this.activity.displayText;
+ this.dateTime = this.activity.time;
+ this.statusText = this.activity.recoveryTipText;
+ }
+ }
+
+ customElements.define("activity-warning-item", ActivityWarningItem, {
+ extends: "li",
+ });
+}
diff --git a/comm/mail/components/activity/content/activity.js b/comm/mail/components/activity/content/activity.js
new file mode 100644
index 0000000000..dcaba3d808
--- /dev/null
+++ b/comm/mail/components/activity/content/activity.js
@@ -0,0 +1,239 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const activityManager = Cc["@mozilla.org/activity-manager;1"].getService(
+ Ci.nsIActivityManager
+);
+
+var ACTIVITY_LIMIT = 250;
+
+var activityObject = {
+ _activityMgrListener: null,
+ _activitiesView: null,
+ _activityLogger: console.createInstance({
+ prefix: "mail.activity",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.activity.loglevel",
+ }),
+ _ignoreNotifications: false,
+ _groupCache: new Map(),
+
+ // Utility Functions for Activity element management
+
+ /**
+ * Creates the proper element for the given activity
+ */
+ createActivityWidget(type) {
+ let element = document.createElement("li", {
+ is: type.bindingName,
+ });
+
+ if (element) {
+ element.setAttribute("actID", type.id);
+ }
+
+ return element;
+ },
+
+ /**
+ * Returns the activity group element that matches the context_type
+ * and context of the given activity, if any.
+ */
+ getActivityGroupElementByContext(aContextType, aContextObj) {
+ return this._groupCache.get(aContextType + ":" + aContextObj);
+ },
+
+ /**
+ * Inserts the given element into the correct position on the
+ * activity manager window.
+ */
+ placeActivityElement(element) {
+ if (element.isGroup || element.isProcess) {
+ this._activitiesView.insertBefore(
+ element,
+ this._activitiesView.firstElementChild
+ );
+ } else {
+ let next = this._activitiesView.firstElementChild;
+ while (next && (next.isWarning || next.isProcess || next.isGroup)) {
+ next = next.nextElementSibling;
+ }
+ if (next) {
+ this._activitiesView.insertBefore(element, next);
+ } else {
+ this._activitiesView.appendChild(element);
+ }
+ }
+ if (element.isGroup) {
+ this._groupCache.set(
+ element.contextType + ":" + element.contextObj,
+ element
+ );
+ }
+ while (this._activitiesView.children.length > ACTIVITY_LIMIT) {
+ this.removeActivityElement(
+ this._activitiesView.lastElementChild.getAttribute("actID")
+ );
+ }
+ },
+
+ /**
+ * Adds a new element to activity manager window for the
+ * given activity. It is called by ActivityMgrListener when
+ * a new activity is added into the activity manager's internal
+ * list.
+ */
+ addActivityElement(aID, aActivity) {
+ try {
+ this._activityLogger.info(`Adding ActivityElement: ${aID}, ${aActivity}`);
+ // get |groupingStyle| of the activity. Grouping style determines
+ // whether we show the activity standalone or grouped by context in
+ // the activity manager window.
+ let isGroupByContext =
+ aActivity.groupingStyle == Ci.nsIActivity.GROUPING_STYLE_BYCONTEXT;
+
+ // find out if an activity group has already been created for this context
+ let group = null;
+ if (isGroupByContext) {
+ group = this.getActivityGroupElementByContext(
+ aActivity.contextType,
+ aActivity.contextObj
+ );
+ // create a group if it's not already created.
+ if (!group) {
+ group = document.createElement("li", {
+ is: "activity-group-item",
+ });
+ this._activityLogger.info("created group element");
+ // Set the context type and object of the newly created group
+ group.contextType = aActivity.contextType;
+ group.contextObj = aActivity.contextObj;
+ group.contextDisplayText = aActivity.contextDisplayText;
+
+ // add group into the list
+ this.placeActivityElement(group);
+ }
+ }
+
+ // create the appropriate element for the activity
+ let actElement = this.createActivityWidget(aActivity);
+ this._activityLogger.info("created activity element");
+
+ if (group) {
+ // get the inner list element of the group
+ let groupView = group.querySelector(".activitygroup-list");
+ groupView.appendChild(actElement);
+ } else {
+ this.placeActivityElement(actElement);
+ }
+ } catch (e) {
+ this._activityLogger.error("addActivityElement: " + e);
+ throw e;
+ }
+ },
+
+ /**
+ * Removes the activity element from the activity manager window.
+ * It is called by ActivityMgrListener when the activity in question
+ * is removed from the activity manager's internal list.
+ */
+ removeActivityElement(aID) {
+ this._activityLogger.info("removing Activity ID: " + aID);
+ let item = this._activitiesView.querySelector(`[actID="${aID}"]`);
+
+ if (item) {
+ let group = item.closest(".activitygroup");
+ item.remove();
+ if (group && !group.querySelector(".activityitem")) {
+ // Empty group is removed.
+ this._groupCache.delete(group.contextType + ":" + group.contextObj);
+ group.remove();
+ }
+ }
+ },
+
+ // -----------------
+ // Startup, Shutdown
+
+ startup() {
+ try {
+ this._activitiesView = document.getElementById("activityView");
+
+ let activities = activityManager.getActivities();
+ for (
+ let iActivity = Math.max(0, activities.length - ACTIVITY_LIMIT);
+ iActivity < activities.length;
+ iActivity++
+ ) {
+ let activity = activities[iActivity];
+ this.addActivityElement(activity.id, activity);
+ }
+
+ // start listening changes in the activity manager's
+ // internal list
+ this._activityMgrListener = new this.ActivityMgrListener();
+ activityManager.addListener(this._activityMgrListener);
+ } catch (e) {
+ this._activityLogger.error("Exception: " + e);
+ }
+ },
+
+ rebuild() {
+ let activities = activityManager.getActivities();
+ for (let activity of activities) {
+ this.addActivityElement(activity.id, activity);
+ }
+ },
+
+ shutdown() {
+ activityManager.removeListener(this._activityMgrListener);
+ },
+
+ // -----------------
+ // Utility Functions
+
+ /**
+ * Remove all activities not in-progress from the activity list.
+ */
+ clearActivityList() {
+ this._activityLogger.debug("clearActivityList");
+
+ this._ignoreNotifications = true;
+ // If/when we implement search, we'll want to remove just the items
+ // that are on the search display, however for now, we'll just clear up
+ // everything.
+ activityManager.cleanUp();
+
+ while (this._activitiesView.lastChild) {
+ this._activitiesView.lastChild.remove();
+ }
+
+ this._groupCache.clear();
+ this.rebuild();
+ this._ignoreNotifications = false;
+ this._activitiesView.focus();
+ },
+};
+
+// An object to monitor nsActivityManager operations. This class acts as
+// binding layer between nsActivityManager and nsActivityManagerUI objects.
+activityObject.ActivityMgrListener = function () {};
+activityObject.ActivityMgrListener.prototype = {
+ onAddedActivity(aID, aActivity) {
+ activityObject._activityLogger.info(`added activity: ${aID} ${aActivity}`);
+ if (!activityObject._ignoreNotifications) {
+ activityObject.addActivityElement(aID, aActivity);
+ }
+ },
+
+ onRemovedActivity(aID) {
+ if (!activityObject._ignoreNotifications) {
+ activityObject.removeActivityElement(aID);
+ }
+ },
+};
+
+window.addEventListener("load", () => activityObject.startup());
+window.addEventListener("unload", () => activityObject.shutdown());
diff --git a/comm/mail/components/activity/content/activity.xhtml b/comm/mail/components/activity/content/activity.xhtml
new file mode 100644
index 0000000000..cdff19cbe6
--- /dev/null
+++ b/comm/mail/components/activity/content/activity.xhtml
@@ -0,0 +1,61 @@
+<?xml version="1.0"?>
+<!-- This Source Code Form is subject to the terms of the Mozilla Public
+ - License, v. 2.0. If a copy of the MPL was not distributed with this
+ - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
+
+#ifdef XP_UNIX
+#ifndef XP_MACOSX
+#define XP_GNOME 1
+#endif
+#endif
+
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/variables.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/activity/activity.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/colors.css"?>
+<?xml-stylesheet href="chrome://messenger/skin/themeableDialog.css"?>
+
+<!DOCTYPE html [
+<!ENTITY % activityManagerDTD SYSTEM "chrome://messenger/locale/activity.dtd">
+%activityManagerDTD;
+]>
+
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ id="activityManager" windowtype="Activity:Manager"
+ width="&window.width2;" height="&window.height;"
+ screenX="10" screenY="10"
+ persist="width height screenX screenY sizemode"
+ lightweightthemes="true">
+<head>
+ <title>&activity.title;</title>
+
+ <script defer="defer" src="chrome://messenger/content/activity.js"></script>
+ <script defer="defer" src="chrome://messenger/content/activity-widgets.js"></script>
+</head>
+<body>
+ <xul:keyset id="activityKeys">
+ <xul:key id="key_close" key="&cmd.close.commandkey;"
+ oncommand="window.close();" modifiers="accel"/>
+#ifdef XP_GNOME
+ <xul:key id="key_close2" key="&cmd.close2Unix.commandkey;"
+ oncommand="window.close();" modifiers="accel"/>
+#else
+ <xul:key id="key_close2" key="&cmd.close2.commandkey;"
+ oncommand="window.close();" modifiers="accel"/>
+#endif
+ <xul:key keycode="VK_ESCAPE" oncommand="window.close();"/>
+ </xul:keyset>
+
+ <div id="activityContainer">
+ <ul id="activityView" class="activityview"></ul>
+ <button id="clearListButton"
+ onclick="activityObject.clearActivityList();"
+ accesskey="&cmd.clearList.accesskey;"
+ title="&cmd.clearList.tooltip;">
+ &cmd.clearList.label;
+ </button>
+ </div>
+</body>
+</html>
diff --git a/comm/mail/components/activity/jar.mn b/comm/mail/components/activity/jar.mn
new file mode 100644
index 0000000000..babeeac23d
--- /dev/null
+++ b/comm/mail/components/activity/jar.mn
@@ -0,0 +1,8 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+messenger.jar:
+ content/messenger/activity.js (content/activity.js)
+ content/messenger/activity-widgets.js (content/activity-widgets.js)
+* content/messenger/activity.xhtml (content/activity.xhtml)
diff --git a/comm/mail/components/activity/modules/activityModules.jsm b/comm/mail/components/activity/modules/activityModules.jsm
new file mode 100644
index 0000000000..945f7473c2
--- /dev/null
+++ b/comm/mail/components/activity/modules/activityModules.jsm
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This module is designed to be a central place to initialise activity related
+// modules.
+
+const EXPORTED_SYMBOLS = [];
+
+const { sendLaterModule } = ChromeUtils.import(
+ "resource:///modules/activity/sendLater.jsm"
+);
+sendLaterModule.init();
+const { moveCopyModule } = ChromeUtils.import(
+ "resource:///modules/activity/moveCopy.jsm"
+);
+moveCopyModule.init();
+const { glodaIndexerActivity } = ChromeUtils.import(
+ "resource:///modules/activity/glodaIndexer.jsm"
+);
+glodaIndexerActivity.init();
+const { autosyncModule } = ChromeUtils.import(
+ "resource:///modules/activity/autosync.jsm"
+);
+autosyncModule.init();
+const { alertHook } = ChromeUtils.import(
+ "resource:///modules/activity/alertHook.jsm"
+);
+alertHook.init();
+const { pop3DownloadModule } = ChromeUtils.import(
+ "resource:///modules/activity/pop3Download.jsm"
+);
+pop3DownloadModule.init();
diff --git a/comm/mail/components/activity/modules/alertHook.jsm b/comm/mail/components/activity/modules/alertHook.jsm
new file mode 100644
index 0000000000..b3083aef0a
--- /dev/null
+++ b/comm/mail/components/activity/modules/alertHook.jsm
@@ -0,0 +1,101 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["alertHook"];
+
+var nsActWarning = Components.Constructor(
+ "@mozilla.org/activity-warning;1",
+ "nsIActivityWarning",
+ "init"
+);
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+
+// This module provides a link between the send later service and the activity
+// manager.
+var alertHook = {
+ get activityMgr() {
+ delete this.activityMgr;
+ return (this.activityMgr = Cc["@mozilla.org/activity-manager;1"].getService(
+ Ci.nsIActivityManager
+ ));
+ },
+
+ get alertService() {
+ delete this.alertService;
+ return (this.alertService = Cc["@mozilla.org/alerts-service;1"].getService(
+ Ci.nsIAlertsService
+ ));
+ },
+
+ get brandShortName() {
+ delete this.brandShortName;
+ return (this.brandShortName = Services.strings
+ .createBundle("chrome://branding/locale/brand.properties")
+ .GetStringFromName("brandShortName"));
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgUserFeedbackListener"]),
+
+ onAlert(aMessage, aUrl) {
+ // Create a new warning.
+ let warning = new nsActWarning(aMessage, this.activityMgr, "");
+
+ if (aUrl && aUrl.server && aUrl.server.prettyName) {
+ warning.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_BYCONTEXT;
+ warning.contextType = "incomingServer";
+ warning.contextDisplayText = aUrl.server.prettyName;
+ warning.contextObj = aUrl.server;
+ } else {
+ warning.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_STANDALONE;
+ }
+
+ this.activityMgr.addActivity(warning);
+
+ // If we have a message window in the url, then show a warning prompt,
+ // just like the modal code used to. Otherwise, don't.
+ try {
+ if (!aUrl || !aUrl.msgWindow) {
+ return true;
+ }
+ } catch (ex) {
+ // nsIMsgMailNewsUrl.msgWindow will throw on a null pointer, so that's
+ // what we're handling here.
+ if (
+ ex instanceof Ci.nsIException &&
+ ex.result == Cr.NS_ERROR_INVALID_POINTER
+ ) {
+ return true;
+ }
+ throw ex;
+ }
+
+ try {
+ let alert = Cc["@mozilla.org/alert-notification;1"].createInstance(
+ Ci.nsIAlertNotification
+ );
+ alert.init(
+ "", // name
+ "chrome://branding/content/icon48.png",
+ this.brandShortName,
+ aMessage
+ );
+ this.alertService.showAlert(alert);
+ } catch (ex) {
+ // XXX On Linux, if libnotify isn't supported, showAlert
+ // can throw an error, so fall-back to the old method of modal dialogs.
+ return false;
+ }
+
+ return true;
+ },
+
+ init() {
+ // We shouldn't need to remove the listener as we're not being held by
+ // anyone except by the send later instance.
+ MailServices.mailSession.addUserFeedbackListener(this);
+ },
+};
diff --git a/comm/mail/components/activity/modules/autosync.jsm b/comm/mail/components/activity/modules/autosync.jsm
new file mode 100644
index 0000000000..c2483c4b53
--- /dev/null
+++ b/comm/mail/components/activity/modules/autosync.jsm
@@ -0,0 +1,433 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["autosyncModule"];
+
+var nsActProcess = Components.Constructor(
+ "@mozilla.org/activity-process;1",
+ "nsIActivityProcess",
+ "init"
+);
+var nsActEvent = Components.Constructor(
+ "@mozilla.org/activity-event;1",
+ "nsIActivityEvent",
+ "init"
+);
+
+/**
+ * This code aims to mediate between the auto-sync code and the activity mgr.
+ *
+ * Not every auto-sync activity is directly mapped to a process or event.
+ * To prevent a possible event overflow, Auto-Sync monitor generates one
+ * sync'd event per account when after all its _pending_ folders are sync'd,
+ * rather than generating one event per folder sync.
+ */
+
+var autosyncModule = {
+ _inQFolderList: [],
+ _running: false,
+ _syncInfoPerFolder: new Map(),
+ _syncInfoPerServer: new Map(),
+ _lastMessage: new Map(),
+
+ get log() {
+ delete this.log;
+ return (this.log = console.createInstance({
+ prefix: "mail.activity",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.activity.loglevel",
+ }));
+ },
+
+ get activityMgr() {
+ delete this.activityMgr;
+ return (this.activityMgr = Cc["@mozilla.org/activity-manager;1"].getService(
+ Ci.nsIActivityManager
+ ));
+ },
+
+ get autoSyncManager() {
+ delete this.autoSyncManager;
+ return (this.autoSyncManager = Cc[
+ "@mozilla.org/imap/autosyncmgr;1"
+ ].getService(Ci.nsIAutoSyncManager));
+ },
+
+ get bundle() {
+ delete this.bundle;
+ return (this.bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/activity.properties"
+ ));
+ },
+
+ getString(stringName) {
+ try {
+ return this.bundle.GetStringFromName(stringName);
+ } catch (e) {
+ this.log.error("error trying to get a string called: " + stringName);
+ throw e;
+ }
+ },
+
+ createSyncMailProcess(folder) {
+ try {
+ // create an activity process for this folder
+ let msg = this.bundle.formatStringFromName("autosyncProcessDisplayText", [
+ folder.prettyName,
+ ]);
+ let process = new nsActProcess(msg, this.autoSyncManager);
+ // we want to use default auto-sync icon
+ process.iconClass = "syncMail";
+ process.addSubject(folder);
+ // group processes under folder's imap account
+ process.contextType = "account";
+ process.contextDisplayText = this.bundle.formatStringFromName(
+ "autosyncContextDisplayText",
+ [folder.server.prettyName]
+ );
+
+ process.contextObj = folder.server;
+
+ return process;
+ } catch (e) {
+ this.log.error("createSyncMailProcess: " + e);
+ throw e;
+ }
+ },
+
+ createSyncMailEvent(syncItem) {
+ try {
+ // extract the relevant parts
+ let process = syncItem.activity;
+ let folder = syncItem.syncFolder;
+
+ // create an activity event
+
+ let msg = this.bundle.formatStringFromName("autosyncEventDisplayText", [
+ folder.server.prettyName,
+ ]);
+
+ let statusMsg;
+ let numOfMessages = this._syncInfoPerServer.get(
+ folder.server
+ ).totalDownloads;
+ if (numOfMessages) {
+ statusMsg = this.bundle.formatStringFromName(
+ "autosyncEventStatusText",
+ [numOfMessages]
+ );
+ } else {
+ statusMsg = this.getString("autosyncEventStatusTextNoMsgs");
+ }
+
+ let event = new nsActEvent(
+ msg,
+ this.autoSyncManager,
+ statusMsg,
+ this._syncInfoPerServer.get(folder.server).startTime,
+ Date.now()
+ ); // completion time
+
+ // since auto-sync events do not have undo option by nature,
+ // setting these values are informational only.
+ event.contextType = process.contextType;
+ event.contextDisplayText = this.bundle.formatStringFromName(
+ "autosyncContextDisplayText",
+ [folder.server.prettyName]
+ );
+ event.contextObj = process.contextObj;
+ event.iconClass = "syncMail";
+
+ // transfer all subjects.
+ // same as above, not mandatory
+ let subjects = process.getSubjects();
+ for (let subject of subjects) {
+ event.addSubject(subject);
+ }
+
+ return event;
+ } catch (e) {
+ this.log.error("createSyncMailEvent: " + e);
+ throw e;
+ }
+ },
+
+ onStateChanged(running) {
+ try {
+ this._running = running;
+ this.log.info(
+ "OnStatusChanged: " + (running ? "running" : "sleeping") + "\n"
+ );
+ } catch (e) {
+ this.log.error("onStateChanged: " + e);
+ throw e;
+ }
+ },
+
+ onFolderAddedIntoQ(queue, folder) {
+ try {
+ if (
+ folder instanceof Ci.nsIMsgFolder &&
+ queue == Ci.nsIAutoSyncMgrListener.PriorityQueue
+ ) {
+ this._inQFolderList.push(folder);
+ this.log.info(
+ "Auto_Sync OnFolderAddedIntoQ [" +
+ this._inQFolderList.length +
+ "] " +
+ folder.prettyName +
+ " of " +
+ folder.server.prettyName
+ );
+ // create an activity process for this folder
+ let process = this.createSyncMailProcess(folder);
+
+ // create a sync object to keep track of the process of this folder
+ let imapFolder = folder.QueryInterface(Ci.nsIMsgImapMailFolder);
+ let syncItem = {
+ syncFolder: folder,
+ activity: process,
+ percentComplete: 0,
+ totalDownloaded: 0,
+ pendingMsgCount: imapFolder.autoSyncStateObj.pendingMessageCount,
+ };
+
+ // if this is the first folder of this server in the queue, then set the sync start time
+ // for activity event
+ if (!this._syncInfoPerServer.has(folder.server)) {
+ this._syncInfoPerServer.set(folder.server, {
+ startTime: Date.now(),
+ totalDownloads: 0,
+ });
+ }
+
+ // associate the sync object with the folder in question
+ // use folder.URI as key
+ this._syncInfoPerFolder.set(folder.URI, syncItem);
+ }
+ } catch (e) {
+ this.log.error("onFolderAddedIntoQ: " + e);
+ throw e;
+ }
+ },
+ onFolderRemovedFromQ(queue, folder) {
+ try {
+ if (
+ folder instanceof Ci.nsIMsgFolder &&
+ queue == Ci.nsIAutoSyncMgrListener.PriorityQueue
+ ) {
+ let i = this._inQFolderList.indexOf(folder);
+ if (i > -1) {
+ this._inQFolderList.splice(i, 1);
+ }
+
+ this.log.info(
+ "OnFolderRemovedFromQ [" +
+ this._inQFolderList.length +
+ "] " +
+ folder.prettyName +
+ " of " +
+ folder.server.prettyName +
+ "\n"
+ );
+
+ let syncItem = this._syncInfoPerFolder.get(folder.URI);
+ let process = syncItem.activity;
+ let canceled = false;
+ if (process instanceof Ci.nsIActivityProcess) {
+ canceled = process.state == Ci.nsIActivityProcess.STATE_CANCELED;
+ process.state = Ci.nsIActivityProcess.STATE_COMPLETED;
+
+ try {
+ this.activityMgr.removeActivity(process.id);
+ } catch (e) {
+ // It is OK to end up here; If the folder is queued and the
+ // message get manually downloaded by the user, we might get
+ // a folder removed notification even before a download
+ // started for this folder. This behavior stems from the fact
+ // that we add activities into the activity manager in
+ // onDownloadStarted notification rather than onFolderAddedIntoQ.
+ // This is an expected side effect.
+ // Log a warning, but do not throw an error.
+ this.log.warn("onFolderRemovedFromQ: " + e);
+ }
+
+ // remove the folder/syncItem association from the table
+ this._syncInfoPerFolder.delete(folder.URI);
+ }
+
+ // if this is the last folder of this server in the queue
+ // create a sync event and clean the sync start time
+ let found = false;
+ for (let value of this._syncInfoPerFolder.values()) {
+ if (value.syncFolder.server == folder.server) {
+ found = true;
+ break;
+ }
+ }
+ this.log.info(
+ "Auto_Sync OnFolderRemovedFromQ Last folder of the server: " + !found
+ );
+ if (!found) {
+ // create an sync event for the completed process if it's not canceled
+ if (!canceled) {
+ let key = folder.server.prettyName;
+ if (
+ this._lastMessage.has(key) &&
+ this.activityMgr.containsActivity(this._lastMessage.get(key))
+ ) {
+ this.activityMgr.removeActivity(this._lastMessage.get(key));
+ }
+ this._lastMessage.set(
+ key,
+ this.activityMgr.addActivity(this.createSyncMailEvent(syncItem))
+ );
+ }
+ this._syncInfoPerServer.delete(folder.server);
+ }
+ }
+ } catch (e) {
+ this.log.error("onFolderRemovedFromQ: " + e);
+ throw e;
+ }
+ },
+ onDownloadStarted(folder, numOfMessages, totalPending) {
+ try {
+ if (folder instanceof Ci.nsIMsgFolder) {
+ this.log.info(
+ "OnDownloadStarted (" +
+ numOfMessages +
+ "/" +
+ totalPending +
+ "): " +
+ folder.prettyName +
+ " of " +
+ folder.server.prettyName +
+ "\n"
+ );
+
+ let syncItem = this._syncInfoPerFolder.get(folder.URI);
+ let process = syncItem.activity;
+
+ // Update the totalPending number. if new messages have been discovered in the folder
+ // after we added the folder into the q, totalPending might be greater than what we have
+ // initially set
+ if (totalPending > syncItem.pendingMsgCount) {
+ syncItem.pendingMsgCount = totalPending;
+ }
+
+ if (process instanceof Ci.nsIActivityProcess) {
+ // if the process has not beed added to activity manager already, add now
+ if (!this.activityMgr.containsActivity(process.id)) {
+ this.log.info(
+ "Auto_Sync OnDownloadStarted: No process, adding a new process"
+ );
+ this.activityMgr.addActivity(process);
+ }
+
+ syncItem.totalDownloaded += numOfMessages;
+
+ process.state = Ci.nsIActivityProcess.STATE_INPROGRESS;
+ let percent =
+ (syncItem.totalDownloaded / syncItem.pendingMsgCount) * 100;
+ if (percent > syncItem.percentComplete) {
+ syncItem.percentComplete = percent;
+ }
+
+ let msg = this.bundle.formatStringFromName(
+ "autosyncProcessProgress2",
+ [
+ syncItem.totalDownloaded,
+ syncItem.pendingMsgCount,
+ folder.prettyName,
+ folder.server.prettyName,
+ ]
+ );
+
+ process.setProgress(
+ msg,
+ syncItem.totalDownloaded,
+ syncItem.pendingMsgCount
+ );
+
+ let serverInfo = this._syncInfoPerServer.get(
+ syncItem.syncFolder.server
+ );
+ serverInfo.totalDownloads += numOfMessages;
+ this._syncInfoPerServer.set(syncItem.syncFolder.server, serverInfo);
+ }
+ }
+ } catch (e) {
+ this.log.error("onDownloadStarted: " + e);
+ throw e;
+ }
+ },
+
+ onDownloadCompleted(folder) {
+ try {
+ if (folder instanceof Ci.nsIMsgFolder) {
+ this.log.info(
+ "OnDownloadCompleted: " +
+ folder.prettyName +
+ " of " +
+ folder.server.prettyName
+ );
+
+ let process = this._syncInfoPerFolder.get(folder.URI).activity;
+ if (process instanceof Ci.nsIActivityProcess && !this._running) {
+ this.log.info(
+ "OnDownloadCompleted: Auto-Sync Manager is paused, pausing the process"
+ );
+ process.state = Ci.nsIActivityProcess.STATE_PAUSED;
+ }
+ }
+ } catch (e) {
+ this.log.error("onDownloadCompleted: " + e);
+ throw e;
+ }
+ },
+
+ onDownloadError(folder) {
+ if (folder instanceof Ci.nsIMsgFolder) {
+ this.log.error(
+ "OnDownloadError: " +
+ folder.prettyName +
+ " of " +
+ folder.server.prettyName +
+ "\n"
+ );
+ }
+ },
+
+ onDiscoveryQProcessed(folder, numOfHdrsProcessed, leftToProcess) {
+ this.log.info(
+ "onDiscoveryQProcessed: Processed " +
+ numOfHdrsProcessed +
+ "/" +
+ (leftToProcess + numOfHdrsProcessed) +
+ " of " +
+ folder.prettyName +
+ "\n"
+ );
+ },
+
+ onAutoSyncInitiated(folder) {
+ this.log.info(
+ "onAutoSyncInitiated: " +
+ folder.prettyName +
+ " of " +
+ folder.server.prettyName +
+ " has been updated.\n"
+ );
+ },
+
+ init() {
+ // XXX when do we need to remove ourselves?
+ this.log.info("initing");
+ Cc["@mozilla.org/imap/autosyncmgr;1"]
+ .getService(Ci.nsIAutoSyncManager)
+ .addListener(this);
+ },
+};
diff --git a/comm/mail/components/activity/modules/glodaIndexer.jsm b/comm/mail/components/activity/modules/glodaIndexer.jsm
new file mode 100644
index 0000000000..5307d5cefa
--- /dev/null
+++ b/comm/mail/components/activity/modules/glodaIndexer.jsm
@@ -0,0 +1,251 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["glodaIndexerActivity"];
+
+var nsActProcess = Components.Constructor(
+ "@mozilla.org/activity-process;1",
+ "nsIActivityProcess",
+ "init"
+);
+var nsActEvent = Components.Constructor(
+ "@mozilla.org/activity-event;1",
+ "nsIActivityEvent",
+ "init"
+);
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ PluralForm: "resource://gre/modules/PluralForm.sys.mjs",
+});
+
+XPCOMUtils.defineLazyModuleGetters(lazy, {
+ Gloda: "resource:///modules/gloda/GlodaPublic.jsm",
+ GlodaConstants: "resource:///modules/gloda/GlodaConstants.jsm",
+ GlodaIndexer: "resource:///modules/gloda/GlodaIndexer.jsm",
+});
+
+/**
+ * Gloda message indexer feedback.
+ */
+var glodaIndexerActivity = {
+ get log() {
+ delete this.log;
+ return (this.log = console.createInstance({
+ prefix: "mail.activity",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.activity.loglevel",
+ }));
+ },
+
+ get activityMgr() {
+ delete this.activityMgr;
+ return (this.activityMgr = Cc["@mozilla.org/activity-manager;1"].getService(
+ Ci.nsIActivityManager
+ ));
+ },
+
+ get bundle() {
+ delete this.bundle;
+ return (this.bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/activity.properties"
+ ));
+ },
+
+ getString(stringName) {
+ try {
+ return this.bundle.GetStringFromName(stringName);
+ } catch (e) {
+ this.log.error("error trying to get a string called: " + stringName);
+ throw e;
+ }
+ },
+
+ init() {
+ // Register a listener with the Gloda indexer that receives notifications
+ // about Gloda indexing status. We wrap the listener in this function so we
+ // can set |this| to the GlodaIndexerActivity object inside the listener.
+ function listenerWrapper(...aArgs) {
+ glodaIndexerActivity.listener(...aArgs);
+ }
+ lazy.GlodaIndexer.addListener(listenerWrapper);
+ },
+
+ /**
+ * Information about the current job. An object with these properties:
+ *
+ * folder {String}
+ * the name of the folder being processed by the job
+ * jobNumber {Number}
+ * the index of the job in the list of jobs
+ * process {nsIActivityProcess}
+ * the activity process corresponding to the current job
+ * startTime {Date}
+ * the time at which we were first notified about the job
+ * totalItemNum {Number}
+ * the total number of messages being indexed in the job
+ * jobType {String}
+ * The IndexinbJob jobType (ex: "folder", "folderCompact")
+ */
+ currentJob: null,
+
+ listener(aStatus, aFolder, aJobNumber, aItemNumber, aTotalItemNum, aJobType) {
+ this.log.debug("Gloda Indexer Folder/Status: " + aFolder + "/" + aStatus);
+ this.log.debug("Gloda Indexer Job: " + aJobNumber);
+ this.log.debug("Gloda Indexer Item: " + aItemNumber + "/" + aTotalItemNum);
+
+ if (aStatus == lazy.GlodaConstants.kIndexerIdle) {
+ if (this.currentJob) {
+ this.onJobCompleted();
+ }
+ } else {
+ // If the job numbers have changed, the indexer has finished the job
+ // we were previously tracking, so convert the corresponding process
+ // into an event and start a new process to track the new job.
+ if (this.currentJob && aJobNumber != this.currentJob.jobNumber) {
+ this.onJobCompleted();
+ }
+
+ // If we aren't tracking a job, either this is the first time we've been
+ // called or the last job we were tracking was completed. Either way,
+ // start tracking the new job.
+ if (!this.currentJob) {
+ this.onJobBegun(aFolder, aJobNumber, aTotalItemNum, aJobType);
+ }
+
+ // If there is only one item, don't bother creating a progress item.
+ if (aTotalItemNum != 1) {
+ this.onJobProgress(aFolder, aItemNumber, aTotalItemNum);
+ }
+ }
+ },
+
+ onJobBegun(aFolder, aJobNumber, aTotalItemNum, aJobType) {
+ let displayText = aFolder
+ ? this.getString("indexingFolder").replace("#1", aFolder)
+ : this.getString("indexing");
+ let process = new nsActProcess(displayText, lazy.Gloda);
+
+ process.iconClass = "indexMail";
+ process.contextType = "account";
+ process.contextObj = aFolder;
+ process.addSubject(aFolder);
+
+ this.currentJob = {
+ folder: aFolder,
+ jobNumber: aJobNumber,
+ process,
+ startTime: new Date(),
+ totalItemNum: aTotalItemNum,
+ jobType: aJobType,
+ };
+
+ this.activityMgr.addActivity(process);
+ },
+
+ onJobProgress(aFolder, aItemNumber, aTotalItemNum) {
+ this.currentJob.process.state = Ci.nsIActivityProcess.STATE_INPROGRESS;
+ // The total number of items being processed in the job can change, as can
+ // the folder being processed, since we sometimes get notified about a job
+ // before it has determined these things, so we update them here.
+ this.currentJob.folder = aFolder;
+ this.currentJob.totalItemNum = aTotalItemNum;
+
+ let statusText;
+ if (aTotalItemNum == null) {
+ statusText = aFolder
+ ? this.getString("indexingFolderStatusVague").replace("#1", aFolder)
+ : this.getString("indexingStatusVague");
+ } else {
+ let percentComplete =
+ aTotalItemNum == 0
+ ? 100
+ : parseInt((aItemNumber / aTotalItemNum) * 100);
+ // Note: we must replace the folder name placeholder last; otherwise,
+ // if the name happens to contain another one of the placeholders, we'll
+ // hork the name when replacing it.
+ statusText = this.getString(
+ aFolder ? "indexingFolderStatusExact" : "indexingStatusExact"
+ );
+ statusText = lazy.PluralForm.get(aTotalItemNum, statusText)
+ .replace("#1", aItemNumber + 1)
+ .replace("#2", aTotalItemNum)
+ .replace("#3", percentComplete)
+ .replace("#4", aFolder);
+ }
+
+ this.currentJob.process.setProgress(statusText, aItemNumber, aTotalItemNum);
+ },
+
+ onJobCompleted() {
+ this.currentJob.process.state = Ci.nsIActivityProcess.STATE_COMPLETED;
+
+ this.activityMgr.removeActivity(this.currentJob.process.id);
+
+ // this.currentJob.totalItemNum might still be null at this point
+ // if we were first notified about the job before the indexer determined
+ // the number of messages to index and then it didn't find any to index.
+ let totalItemNum = this.currentJob.totalItemNum || 0;
+
+ // We only create activity events when specific folders get indexed,
+ // since event-driven indexing jobs are too numerous. We also only create
+ // them when we ended up indexing something in the folder, since otherwise
+ // we'd spam the activity manager with too many "indexed 0 messages" items
+ // that aren't useful enough to justify their presence in the manager.
+ // TODO: Aggregate event-driven indexing jobs into batches significant
+ // enough for us to create activity events for them.
+ if (
+ this.currentJob.jobType == "folder" &&
+ this.currentJob.folder &&
+ totalItemNum > 0
+ ) {
+ // Note: we must replace the folder name placeholder last; otherwise,
+ // if the name happens to contain another one of the placeholders, we'll
+ // hork the name when replacing it.
+ let displayText = lazy.PluralForm.get(
+ totalItemNum,
+ this.getString("indexedFolder")
+ )
+ .replace("#1", totalItemNum)
+ .replace("#2", this.currentJob.folder);
+
+ let endTime = new Date();
+ let secondsElapsed = parseInt(
+ (endTime - this.currentJob.startTime) / 1000
+ );
+
+ let statusText = lazy.PluralForm.get(
+ secondsElapsed,
+ this.getString("indexedFolderStatus")
+ ).replace("#1", secondsElapsed);
+
+ let event = new nsActEvent(
+ displayText,
+ lazy.Gloda,
+ statusText,
+ this.currentJob.startTime,
+ endTime
+ );
+ event.contextType = this.currentJob.contextType;
+ event.contextObj = this.currentJob.contextObj;
+ event.iconClass = "indexMail";
+
+ // Transfer subjects.
+ let subjects = this.currentJob.process.getSubjects();
+ for (let subject of subjects) {
+ event.addSubject(subject);
+ }
+
+ this.activityMgr.addActivity(event);
+ }
+
+ this.currentJob = null;
+ },
+};
diff --git a/comm/mail/components/activity/modules/moveCopy.jsm b/comm/mail/components/activity/modules/moveCopy.jsm
new file mode 100644
index 0000000000..de3e51d85b
--- /dev/null
+++ b/comm/mail/components/activity/modules/moveCopy.jsm
@@ -0,0 +1,396 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["moveCopyModule"];
+
+var nsActEvent = Components.Constructor(
+ "@mozilla.org/activity-event;1",
+ "nsIActivityEvent",
+ "init"
+);
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { PluralForm } = ChromeUtils.importESModule(
+ "resource://gre/modules/PluralForm.sys.mjs"
+);
+
+// This module provides a link between the move/copy code and the activity
+// manager.
+var moveCopyModule = {
+ lastMessage: {},
+ lastFolder: {},
+
+ get log() {
+ delete this.log;
+ return (this.log = console.createInstance({
+ prefix: "mail.activity",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.activity.loglevel",
+ }));
+ },
+
+ get activityMgr() {
+ delete this.activityMgr;
+ return (this.activityMgr = Cc["@mozilla.org/activity-manager;1"].getService(
+ Ci.nsIActivityManager
+ ));
+ },
+
+ get bundle() {
+ delete this.bundle;
+ return (this.bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/activity.properties"
+ ));
+ },
+
+ getString(stringName) {
+ try {
+ return this.bundle.GetStringFromName(stringName);
+ } catch (e) {
+ this.log.error("error trying to get a string called: " + stringName);
+ throw e;
+ }
+ },
+
+ msgAdded(aMsg) {},
+
+ msgsDeleted(aMsgList) {
+ this.log.info("in msgsDeleted");
+
+ if (aMsgList.length <= 0) {
+ return;
+ }
+
+ let displayCount = aMsgList.length;
+ // get the folder of the deleted messages
+ let folder = aMsgList[0].folder;
+
+ let activities = this.activityMgr.getActivities();
+ if (
+ activities.length > 0 &&
+ activities[activities.length - 1].id == this.lastMessage.id &&
+ this.lastMessage.type == "deleteMail" &&
+ this.lastMessage.folder == folder.prettyName
+ ) {
+ displayCount += this.lastMessage.count;
+ this.activityMgr.removeActivity(this.lastMessage.id);
+ }
+
+ this.lastMessage = {};
+ let displayText = PluralForm.get(
+ displayCount,
+ this.getString("deletedMessages2")
+ );
+ displayText = displayText.replace("#1", displayCount);
+ this.lastMessage.count = displayCount;
+ displayText = displayText.replace("#2", folder.prettyName);
+ this.lastMessage.folder = folder.prettyName;
+
+ let statusText = folder.server.prettyName;
+
+ // create an activity event
+ let event = new nsActEvent(
+ displayText,
+ folder,
+ statusText,
+ Date.now(), // start time
+ Date.now()
+ ); // completion time
+
+ event.iconClass = "deleteMail";
+ this.lastMessage.type = event.iconClass;
+
+ for (let msgHdr of aMsgList) {
+ event.addSubject(msgHdr.messageId);
+ }
+
+ this.lastMessage.id = this.activityMgr.addActivity(event);
+ },
+
+ msgsMoveCopyCompleted(aMove, aSrcMsgList, aDestFolder) {
+ try {
+ this.log.info("in msgsMoveCopyCompleted");
+
+ let count = aSrcMsgList.length;
+ if (count <= 0) {
+ return;
+ }
+
+ // get the folder of the moved/copied messages
+ let folder = aSrcMsgList[0].folder;
+ this.log.info("got folder");
+
+ let displayCount = count;
+
+ let activities = this.activityMgr.getActivities();
+ if (
+ activities.length > 0 &&
+ activities[activities.length - 1].id == this.lastMessage.id &&
+ this.lastMessage.type == (aMove ? "moveMail" : "copyMail") &&
+ this.lastMessage.sourceFolder == folder.prettyName &&
+ this.lastMessage.destFolder == aDestFolder.prettyName
+ ) {
+ displayCount += this.lastMessage.count;
+ this.activityMgr.removeActivity(this.lastMessage.id);
+ }
+
+ let statusText = "";
+ if (folder.server != aDestFolder.server) {
+ statusText = this.getString("fromServerToServer");
+ statusText = statusText.replace("#1", folder.server.prettyName);
+ statusText = statusText.replace("#2", aDestFolder.server.prettyName);
+ } else {
+ statusText = folder.server.prettyName;
+ }
+
+ this.lastMessage = {};
+ let displayText;
+ if (aMove) {
+ displayText = PluralForm.get(
+ displayCount,
+ this.getString("movedMessages")
+ );
+ } else {
+ displayText = PluralForm.get(
+ displayCount,
+ this.getString("copiedMessages")
+ );
+ }
+
+ displayText = displayText.replace("#1", displayCount);
+ this.lastMessage.count = displayCount;
+ displayText = displayText.replace("#2", folder.prettyName);
+ this.lastMessage.sourceFolder = folder.prettyName;
+ displayText = displayText.replace("#3", aDestFolder.prettyName);
+ this.lastMessage.destFolder = aDestFolder.prettyName;
+
+ // create an activity event
+ let event = new nsActEvent(
+ displayText,
+ folder,
+ statusText,
+ Date.now(), // start time
+ Date.now()
+ ); // completion time
+ event.iconClass = aMove ? "moveMail" : "copyMail";
+ this.lastMessage.type = event.iconClass;
+
+ for (let msgHdr of aSrcMsgList) {
+ event.addSubject(msgHdr.messageId);
+ }
+ this.lastMessage.id = this.activityMgr.addActivity(event);
+ } catch (e) {
+ this.log.error("Exception: " + e);
+ }
+ },
+
+ folderAdded(aFolder) {},
+
+ folderDeleted(aFolder) {
+ // When a new account is created we get this notification with an empty named
+ // folder that can't return its server. Ignore it.
+ // TODO: find out what it is.
+ let server = aFolder.server;
+ // If the account has been removed, we're going to ignore this notification.
+ if (
+ !MailServices.accounts.findServer(
+ server.username,
+ server.hostName,
+ server.type
+ )
+ ) {
+ return;
+ }
+
+ let displayText;
+ let statusText = server.prettyName;
+
+ // Display a different message depending on whether we emptied the trash
+ // or actually deleted a folder
+ if (aFolder.isSpecialFolder(Ci.nsMsgFolderFlags.Trash, false)) {
+ displayText = this.getString("emptiedTrash");
+ } else {
+ displayText = this.getString("deletedFolder").replace(
+ "#1",
+ aFolder.prettyName
+ );
+ }
+
+ // create an activity event
+ let event = new nsActEvent(
+ displayText,
+ server,
+ statusText,
+ Date.now(), // start time
+ Date.now()
+ ); // completion time
+
+ event.addSubject(aFolder);
+ event.iconClass = "deleteMail";
+
+ // When we rename, we get a delete event as well as a rename, so store
+ // the last folder we deleted
+ this.lastFolder = {};
+ this.lastFolder.URI = aFolder.URI;
+ this.lastFolder.event = this.activityMgr.addActivity(event);
+ },
+
+ folderMoveCopyCompleted(aMove, aSrcFolder, aDestFolder) {
+ this.log.info("in folderMoveCopyCompleted, aMove = " + aMove);
+
+ let displayText;
+ if (aMove) {
+ displayText = this.getString("movedFolder");
+ } else {
+ displayText = this.getString("copiedFolder");
+ }
+
+ displayText = displayText.replace("#1", aSrcFolder.prettyName);
+ displayText = displayText.replace("#2", aDestFolder.prettyName);
+
+ let statusText = "";
+ if (aSrcFolder.server != aDestFolder.server) {
+ statusText = this.getString("fromServerToServer");
+ statusText = statusText.replace("#1", aSrcFolder.server.prettyName);
+ statusText = statusText.replace("#2", aDestFolder.server.prettyName);
+ } else {
+ statusText = aSrcFolder.server.prettyName;
+ }
+ // create an activity event
+ let event = new nsActEvent(
+ displayText,
+ aSrcFolder.server,
+ statusText,
+ Date.now(), // start time
+ Date.now()
+ ); // completion time
+
+ event.addSubject(aSrcFolder);
+ event.addSubject(aDestFolder);
+ event.iconClass = aMove ? "moveMail" : "copyMail";
+
+ this.activityMgr.addActivity(event);
+ },
+
+ folderRenamed(aOrigFolder, aNewFolder) {
+ this.log.info(
+ "in folderRenamed, aOrigFolder = " +
+ aOrigFolder.prettyName +
+ ", aNewFolder = " +
+ aNewFolder.prettyName
+ );
+
+ let displayText;
+ let statusText = aNewFolder.server.prettyName;
+
+ // Display a different message depending on whether we moved the folder
+ // to the trash or actually renamed the folder.
+ if (aNewFolder.isSpecialFolder(Ci.nsMsgFolderFlags.Trash, true)) {
+ displayText = this.getString("movedFolderToTrash");
+ displayText = displayText.replace("#1", aOrigFolder.prettyName);
+ } else {
+ displayText = this.getString("renamedFolder");
+ displayText = displayText.replace("#1", aOrigFolder.prettyName);
+ displayText = displayText.replace("#2", aNewFolder.prettyName);
+ }
+
+ // When renaming a folder, a delete event is always fired first
+ if (this.lastFolder.URI == aOrigFolder.URI) {
+ this.activityMgr.removeActivity(this.lastFolder.event);
+ }
+
+ // create an activity event
+ let event = new nsActEvent(
+ displayText,
+ aOrigFolder.server,
+ statusText,
+ Date.now(), // start time
+ Date.now()
+ ); // completion time
+
+ event.addSubject(aOrigFolder);
+ event.addSubject(aNewFolder);
+
+ this.activityMgr.addActivity(event);
+ },
+
+ msgUnincorporatedMoved(srcFolder, msgHdr) {
+ try {
+ this.log.info("in msgUnincorporatedMoved");
+
+ // get the folder of the moved/copied messages
+ let destFolder = msgHdr.folder;
+ this.log.info("got folder");
+
+ let displayCount = 1;
+
+ let activities = this.activityMgr.getActivities();
+ if (
+ activities.length > 0 &&
+ activities[activities.length - 1].id == this.lastMessage.id &&
+ this.lastMessage.type == "moveMail" &&
+ this.lastMessage.sourceFolder == srcFolder.prettyName &&
+ this.lastMessage.destFolder == destFolder.prettyName
+ ) {
+ displayCount += this.lastMessage.count;
+ this.activityMgr.removeActivity(this.lastMessage.id);
+ }
+
+ let statusText = "";
+ if (srcFolder.server != destFolder.server) {
+ statusText = this.getString("fromServerToServer");
+ statusText = statusText.replace("#1", srcFolder.server.prettyName);
+ statusText = statusText.replace("#2", destFolder.server.prettyName);
+ } else {
+ statusText = srcFolder.server.prettyName;
+ }
+
+ this.lastMessage = {};
+ let displayText;
+ displayText = PluralForm.get(
+ displayCount,
+ this.getString("movedMessages")
+ );
+
+ displayText = displayText.replace("#1", displayCount);
+ this.lastMessage.count = displayCount;
+ displayText = displayText.replace("#2", srcFolder.prettyName);
+ this.lastMessage.sourceFolder = srcFolder.prettyName;
+ displayText = displayText.replace("#3", destFolder.prettyName);
+ this.lastMessage.destFolder = destFolder.prettyName;
+
+ // create an activity event
+ let event = new nsActEvent(
+ displayText,
+ srcFolder,
+ statusText,
+ Date.now(), // start time
+ Date.now()
+ ); // completion time
+
+ event.iconClass = "moveMail";
+ this.lastMessage.type = event.iconClass;
+ event.addSubject(msgHdr.messageId);
+ this.lastMessage.id = this.activityMgr.addActivity(event);
+ } catch (e) {
+ this.log.error("Exception: " + e);
+ }
+ },
+
+ init() {
+ // XXX when do we need to remove ourselves?
+ MailServices.mfn.addListener(
+ this,
+ MailServices.mfn.msgsDeleted |
+ MailServices.mfn.msgsMoveCopyCompleted |
+ MailServices.mfn.folderDeleted |
+ MailServices.mfn.folderMoveCopyCompleted |
+ MailServices.mfn.folderRenamed |
+ MailServices.mfn.msgUnincorporatedMoved
+ );
+ },
+};
diff --git a/comm/mail/components/activity/modules/pop3Download.jsm b/comm/mail/components/activity/modules/pop3Download.jsm
new file mode 100644
index 0000000000..f203b33212
--- /dev/null
+++ b/comm/mail/components/activity/modules/pop3Download.jsm
@@ -0,0 +1,154 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["pop3DownloadModule"];
+
+var nsActEvent = Components.Constructor(
+ "@mozilla.org/activity-event;1",
+ "nsIActivityEvent",
+ "init"
+);
+
+const { MailServices } = ChromeUtils.import(
+ "resource:///modules/MailServices.jsm"
+);
+const { PluralForm } = ChromeUtils.importESModule(
+ "resource://gre/modules/PluralForm.sys.mjs"
+);
+
+// This module provides a link between the pop3 service code and the activity
+// manager.
+var pop3DownloadModule = {
+ // hash table of most recent download items per folder
+ _mostRecentActivityForFolder: new Map(),
+ // hash table of prev download items per folder, so we can
+ // coalesce consecutive no new message events.
+ _prevActivityForFolder: new Map(),
+
+ get log() {
+ delete this.log;
+ return (this.log = console.createInstance({
+ prefix: "mail.activity",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.activity.loglevel",
+ }));
+ },
+
+ get activityMgr() {
+ delete this.activityMgr;
+ return (this.activityMgr = Cc["@mozilla.org/activity-manager;1"].getService(
+ Ci.nsIActivityManager
+ ));
+ },
+
+ get bundle() {
+ delete this.bundle;
+ return (this.bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/activity.properties"
+ ));
+ },
+
+ getString(stringName) {
+ try {
+ return this.bundle.GetStringFromName(stringName);
+ } catch (e) {
+ this.log.error("error trying to get a string called: " + stringName);
+ throw e;
+ }
+ },
+
+ onDownloadStarted(aFolder) {
+ this.log.info("in onDownloadStarted");
+
+ let displayText = this.bundle.formatStringFromName(
+ "pop3EventStartDisplayText2",
+ [
+ aFolder.server.prettyName, // account name
+ aFolder.prettyName,
+ ]
+ ); // folder name
+ // remember the prev activity for this folder, if any.
+ this._prevActivityForFolder.set(
+ aFolder.URI,
+ this._mostRecentActivityForFolder.get(aFolder.URI)
+ );
+ let statusText = aFolder.server.prettyName;
+
+ // create an activity event
+ let event = new nsActEvent(
+ displayText,
+ aFolder,
+ statusText,
+ Date.now(), // start time
+ Date.now()
+ ); // completion time
+
+ event.iconClass = "syncMail";
+
+ let downloadItem = {};
+ downloadItem.eventID = this.activityMgr.addActivity(event);
+ this._mostRecentActivityForFolder.set(aFolder.URI, downloadItem);
+ },
+
+ onDownloadProgress(aFolder, aNumMsgsDownloaded, aTotalMsgs) {
+ this.log.info("in onDownloadProgress");
+ },
+
+ onDownloadCompleted(aFolder, aNumMsgsDownloaded) {
+ this.log.info("in onDownloadCompleted");
+
+ // Remove activity if there was any.
+ // It can happen that download never started (e.g. couldn't connect to server),
+ // with onDownloadStarted, but we still get a onDownloadCompleted event
+ // when the connection is given up.
+ let recentActivity = this._mostRecentActivityForFolder.get(aFolder.URI);
+ if (recentActivity) {
+ this.activityMgr.removeActivity(recentActivity.eventID);
+ }
+
+ let displayText;
+ if (aNumMsgsDownloaded > 0) {
+ displayText = PluralForm.get(
+ aNumMsgsDownloaded,
+ this.getString("pop3EventStatusText")
+ );
+ displayText = displayText.replace("#1", aNumMsgsDownloaded);
+ } else {
+ displayText = this.getString("pop3EventStatusTextNoMsgs");
+ }
+
+ let statusText = aFolder.server.prettyName;
+
+ // create an activity event
+ let event = new nsActEvent(
+ displayText,
+ aFolder,
+ statusText,
+ Date.now(), // start time
+ Date.now()
+ ); // completion time
+
+ event.iconClass = "syncMail";
+
+ let downloadItem = { numMsgsDownloaded: aNumMsgsDownloaded };
+ this._mostRecentActivityForFolder.set(aFolder.URI, downloadItem);
+ downloadItem.eventID = this.activityMgr.addActivity(event);
+ if (!aNumMsgsDownloaded) {
+ // If we didn't download any messages this time, and the prev event
+ // for this folder also didn't download any messages, remove the
+ // prev event from the activity manager.
+ let prevItem = this._prevActivityForFolder.get(aFolder.URI);
+ if (prevItem != undefined && !prevItem.numMsgsDownloaded) {
+ if (this.activityMgr.containsActivity(prevItem.eventID)) {
+ this.activityMgr.removeActivity(prevItem.eventID);
+ }
+ }
+ }
+ },
+ init() {
+ // XXX when do we need to remove ourselves?
+ MailServices.pop3.addListener(this);
+ },
+};
diff --git a/comm/mail/components/activity/modules/sendLater.jsm b/comm/mail/components/activity/modules/sendLater.jsm
new file mode 100644
index 0000000000..37027d96f1
--- /dev/null
+++ b/comm/mail/components/activity/modules/sendLater.jsm
@@ -0,0 +1,298 @@
+/* -*- Mode: JavaScript; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const EXPORTED_SYMBOLS = ["sendLaterModule"];
+
+var nsActProcess = Components.Constructor(
+ "@mozilla.org/activity-process;1",
+ "nsIActivityProcess",
+ "init"
+);
+var nsActEvent = Components.Constructor(
+ "@mozilla.org/activity-event;1",
+ "nsIActivityEvent",
+ "init"
+);
+var nsActWarning = Components.Constructor(
+ "@mozilla.org/activity-warning;1",
+ "nsIActivityWarning",
+ "init"
+);
+
+/**
+ * This really, really, sucks. Due to mailnews widespread use of
+ * nsIMsgStatusFeedback we're bound to the UI to get any sensible feedback of
+ * mail sending operations. The current send later code can't hook into the
+ * progress listener easily to get the state of messages being sent, so we'll
+ * just have to do it here.
+ */
+var sendMsgProgressListener = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIMsgStatusFeedback",
+ "nsISupportsWeakReference",
+ ]),
+
+ showStatusString(aStatusText) {
+ sendLaterModule.onMsgStatus(aStatusText);
+ },
+
+ startMeteors() {},
+
+ stopMeteors() {},
+
+ showProgress(aPercentage) {
+ sendLaterModule.onMessageSendProgress(0, 0, aPercentage, 0);
+ },
+};
+
+// This module provides a link between the send later service and the activity
+// manager.
+var sendLaterModule = {
+ _sendProcess: null,
+ _copyProcess: null,
+ _identity: null,
+ _subject: null,
+
+ get log() {
+ delete this.log;
+ return (this.log = console.createInstance({
+ prefix: "mail.activity",
+ maxLogLevel: "Warn",
+ maxLogLevelPref: "mail.activity.loglevel",
+ }));
+ },
+
+ get activityMgr() {
+ delete this.activityMgr;
+ return (this.activityMgr = Cc["@mozilla.org/activity-manager;1"].getService(
+ Ci.nsIActivityManager
+ ));
+ },
+
+ get bundle() {
+ delete this.bundle;
+ return (this.bundle = Services.strings.createBundle(
+ "chrome://messenger/locale/activity.properties"
+ ));
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIMsgSendLaterListener"]),
+
+ _displayTextForHeader(aLocaleStringBase, aSubject) {
+ return aSubject
+ ? this.bundle.formatStringFromName(aLocaleStringBase + "WithSubject", [
+ aSubject,
+ ])
+ : this.bundle.GetStringFromName(aLocaleStringBase);
+ },
+
+ _newProcess(aLocaleStringBase, aAddSubject) {
+ let process = new nsActProcess(
+ this._displayTextForHeader(
+ aLocaleStringBase,
+ aAddSubject ? this._subject : ""
+ ),
+ this.activityMgr
+ );
+
+ process.iconClass = "sendMail";
+ process.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_BYCONTEXT;
+ process.contextObj = this;
+ process.contextType = "SendLater";
+ process.contextDisplayText =
+ this.bundle.GetStringFromName("sendingMessages");
+
+ return process;
+ },
+
+ // Use this to group an activity by the identity if we have one.
+ _applyIdentityGrouping(aActivity) {
+ if (this._identity) {
+ aActivity.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_BYCONTEXT;
+ aActivity.contextType = this._identity.key;
+ aActivity.contextObj = this._identity;
+ let contextDisplayText = this._identity.identityName;
+ if (!contextDisplayText) {
+ contextDisplayText = this._identity.email;
+ }
+
+ aActivity.contextDisplayText = contextDisplayText;
+ } else {
+ aActivity.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_STANDALONE;
+ }
+ },
+
+ // Replaces the process with an event that reflects a completed process.
+ _replaceProcessWithEvent(aProcess) {
+ this.activityMgr.removeActivity(aProcess.id);
+
+ let event = new nsActEvent(
+ this._displayTextForHeader("sentMessage", this._subject),
+ this.activityMgr,
+ "",
+ aProcess.startTime,
+ new Date()
+ );
+
+ event.iconClass = "sendMail";
+ this._applyIdentityGrouping(event);
+
+ this.activityMgr.addActivity(event);
+ },
+
+ // Replaces the process with a warning that reflects the failed process.
+ _replaceProcessWithWarning(
+ aProcess,
+ aCopyOrSend,
+ aStatus,
+ aMsg,
+ aMessageHeader
+ ) {
+ this.activityMgr.removeActivity(aProcess.id);
+
+ let warning = new nsActWarning(
+ this._displayTextForHeader("failedTo" + aCopyOrSend, this._subject),
+ this.activityMgr,
+ ""
+ );
+
+ warning.groupingStyle = Ci.nsIActivity.GROUPING_STYLE_STANDALONE;
+ this._applyIdentityGrouping(warning);
+
+ this.activityMgr.addActivity(warning);
+ },
+
+ onStartSending(aTotalMessageCount) {
+ if (!aTotalMessageCount) {
+ this.log.error("onStartSending called with zero messages\n");
+ }
+ },
+
+ onMessageStartSending(
+ aCurrentMessage,
+ aTotalMessageCount,
+ aMessageHeader,
+ aIdentity
+ ) {
+ // We want to use the identity and subject later, so store them for now.
+ this._identity = aIdentity;
+ if (aMessageHeader) {
+ this._subject = aMessageHeader.mime2DecodedSubject;
+ }
+
+ // Create the process to display the send activity.
+ let process = this._newProcess("sendingMessage", true);
+ this._sendProcess = process;
+ this.activityMgr.addActivity(process);
+
+ // Now the one for the copy process.
+ process = this._newProcess("copyMessage", false);
+ this._copyProcess = process;
+ this.activityMgr.addActivity(process);
+ },
+
+ onMessageSendProgress(
+ aCurrentMessage,
+ aTotalMessageCount,
+ aMessageSendPercent,
+ aMessageCopyPercent
+ ) {
+ if (aMessageSendPercent < 100) {
+ // Ensure we are in progress...
+ if (this._sendProcess.state != Ci.nsIActivityProcess.STATE_INPROGRESS) {
+ this._sendProcess.state = Ci.nsIActivityProcess.STATE_INPROGRESS;
+ }
+
+ // ... and update the progress.
+ this._sendProcess.setProgress(
+ this._sendProcess.lastStatusText,
+ aMessageSendPercent,
+ 100
+ );
+ } else if (aMessageSendPercent == 100) {
+ if (aMessageCopyPercent == 0) {
+ // Set send state to completed
+ if (this._sendProcess.state != Ci.nsIActivityProcess.STATE_COMPLETED) {
+ this._sendProcess.state = Ci.nsIActivityProcess.STATE_COMPLETED;
+ }
+ this._replaceProcessWithEvent(this._sendProcess);
+
+ // Set copy state to in progress.
+ if (this._copyProcess.state != Ci.nsIActivityProcess.STATE_INPROGRESS) {
+ this._copyProcess.state = Ci.nsIActivityProcess.STATE_INPROGRESS;
+ }
+
+ // We don't know the progress of the copy, so just set to 0, and we'll
+ // display an undetermined progress meter.
+ this._copyProcess.setProgress(this._copyProcess.lastStatusText, 0, 0);
+ } else if (aMessageCopyPercent >= 100) {
+ // We need to set this to completed otherwise activity manager
+ // complains.
+ if (this._copyProcess) {
+ this._copyProcess.state = Ci.nsIActivityProcess.STATE_COMPLETED;
+ this.activityMgr.removeActivity(this._copyProcess.id);
+ this._copyProcess = null;
+ }
+
+ this._sendProcess = null;
+ }
+ }
+ },
+
+ onMessageSendError(aCurrentMessage, aMessageHeader, aStatus, aMsg) {
+ if (
+ this._sendProcess &&
+ this._sendProcess.state != Ci.nsIActivityProcess.STATE_COMPLETED
+ ) {
+ this._sendProcess.state = Ci.nsIActivityProcess.STATE_COMPLETED;
+ this._replaceProcessWithWarning(
+ this._sendProcess,
+ "SendMessage",
+ aStatus,
+ aMsg,
+ aMessageHeader
+ );
+ this._sendProcess = null;
+
+ if (
+ this._copyProcess &&
+ this._copyProcess.state != Ci.nsIActivityProcess.STATE_COMPLETED
+ ) {
+ this._copyProcess.state = Ci.nsIActivityProcess.STATE_COMPLETED;
+ this.activityMgr.removeActivity(this._copyProcess.id);
+ this._copyProcess = null;
+ }
+ }
+ },
+
+ onMsgStatus(aStatusText) {
+ this._sendProcess.setProgress(
+ aStatusText,
+ this._sendProcess.workUnitComplete,
+ this._sendProcess.totalWorkUnits
+ );
+ },
+
+ onStopSending(aStatus, aMsg, aTotalTried, aSuccessful) {},
+
+ init() {
+ // We should need to remove the listener as we're not being held by anyone
+ // except by the send later instance.
+ let sendLaterService = Cc[
+ "@mozilla.org/messengercompose/sendlater;1"
+ ].getService(Ci.nsIMsgSendLater);
+
+ sendLaterService.addListener(this);
+
+ // Also add the nsIMsgStatusFeedback object.
+ let statusFeedback = Cc[
+ "@mozilla.org/messenger/statusfeedback;1"
+ ].createInstance(Ci.nsIMsgStatusFeedback);
+
+ statusFeedback.setWrappedStatusFeedback(sendMsgProgressListener);
+
+ sendLaterService.statusFeedback = statusFeedback;
+ },
+};
diff --git a/comm/mail/components/activity/moz.build b/comm/mail/components/activity/moz.build
new file mode 100644
index 0000000000..efceaacf9f
--- /dev/null
+++ b/comm/mail/components/activity/moz.build
@@ -0,0 +1,34 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPIDL_SOURCES += [
+ "nsIActivity.idl",
+ "nsIActivityManager.idl",
+ "nsIActivityManagerUI.idl",
+]
+
+XPIDL_MODULE = "activity"
+
+EXTRA_JS_MODULES.activity += [
+ "modules/activityModules.jsm",
+ "modules/alertHook.jsm",
+ "modules/autosync.jsm",
+ "modules/glodaIndexer.jsm",
+ "modules/moveCopy.jsm",
+ "modules/pop3Download.jsm",
+ "modules/sendLater.jsm",
+]
+
+EXTRA_JS_MODULES += [
+ "Activity.jsm",
+ "ActivityManager.jsm",
+ "ActivityManagerUI.jsm",
+]
+
+JAR_MANIFESTS += ["jar.mn"]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
diff --git a/comm/mail/components/activity/nsIActivity.idl b/comm/mail/components/activity/nsIActivity.idl
new file mode 100644
index 0000000000..d80e69088f
--- /dev/null
+++ b/comm/mail/components/activity/nsIActivity.idl
@@ -0,0 +1,492 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "nsISupportsPrimitives.idl"
+
+interface nsIActivityListener;
+interface nsIActivity;
+interface nsIActivityProcess;
+interface nsIActivityEvent;
+interface nsIActivityWarning;
+interface nsIVariant;
+interface nsISupportsPRTime;
+
+/**
+ * See https://wiki.mozilla.org/Thunderbird:Activity_Manager/Developer for UML
+ * diagram and sample codes.
+ */
+
+/**
+ * Background:
+ * Activity handlers define the behavioral capabilities of the activities. They
+ * are used by the Activity Manager to change the execution flow of the activity
+ * based on the user interaction. They are not mandatory, but when set, causes
+ * behavioral changes on the binding representing the activity, such as showing
+ * a cancel button, etc. The following handlers are currently supported;
+ */
+
+/**
+ * The handler to invoke when the recover button is pressed. Used by a Warning
+ * to recover from the situation causing the warning. For instance, recovery
+ * action for a "Over Quota Limit" warning, would be to cleanup some disk space,
+ * and this operation can be implemented and set by the activity developer in
+ * form of nsIActivityRecoveryHandler component.
+ */
+[scriptable, uuid(30E0A76F-880A-4093-8F3C-AF2239977A3D)]
+interface nsIActivityRecoveryHandler : nsISupports {
+ nsresult recover(in nsIActivityWarning aActivity);
+};
+
+/**
+ * The handler to invoke when the cancel button is pressed. Used by a Process to
+ * cancel the operation.
+ */
+[scriptable, uuid(35ee2461-70db-4b3a-90d0-7a68c856e911)]
+interface nsIActivityCancelHandler : nsISupports {
+ nsresult cancel(in nsIActivityProcess aActivity);
+};
+
+/**
+ * The handler to invoke when the pause button is pressed. Used by a Process to
+ * pause/resume the operation.
+ */
+[scriptable, uuid(9eee22bf-5378-460e-83a7-781cdcc9050b)]
+interface nsIActivityPauseHandler : nsISupports {
+ nsresult pause(in nsIActivityProcess aActivity);
+ nsresult resume(in nsIActivityProcess aActivity);
+};
+
+/**
+ * The handler to invoke when the retry button is pressed. Used by a Process to
+ * retry the operation in case of failure.
+ */
+[scriptable, uuid(8ec42517-951f-4bc0-aba5-fde7258b1705)]
+interface nsIActivityRetryHandler : nsISupports {
+ nsresult retry(in nsIActivityProcess aActivity);
+};
+
+/**
+ * The handler to invoke when the undo button is pressed. Used by a Event to
+ * undo the operation generated the event.
+ */
+[scriptable, uuid(b8632ac7-9d8b-4341-a349-ef000e8c89ac)]
+interface nsIActivityUndoHandler : nsISupports {
+ nsresult undo(in nsIActivityEvent aActivity);
+};
+
+/**
+ * Base interface of all activity interfaces. It is abstract in a sense that
+ * there is no component in the activity management system that solely
+ * implements this interface.
+ */
+[scriptable, uuid(6CD33E65-B2D8-4634-9B6D-B80BF1273E99)]
+interface nsIActivity : nsISupports {
+
+ /**
+ * Shows the activity as a standalone item.
+ */
+ const short GROUPING_STYLE_STANDALONE = 1;
+
+ /**
+ * Groups activity by its context.
+ */
+ const short GROUPING_STYLE_BYCONTEXT = 2;
+
+ /**
+ * Internal ID given by the activity manager when
+ * added into the activity list. Not readonly so that
+ * the activity manager can write to them, but not to be written to
+ * by anyone else.
+ */
+ attribute unsigned long id;
+
+ // Following attributes change the UI characteristics of the activity
+
+ /**
+ * A brief description of the activity, to be shown by the
+ * associated binding (XBL) in the Activity Manager window.
+ */
+ readonly attribute AString displayText;
+
+ /**
+ * Changes the default icon associated with the activity. Core activity
+ * icons are declared in |mail/themes/<themename>/mail/activity/activity.css|
+ * files.
+ *
+ * Extension developers can add and assign their own icons by setting
+ * this attribute.
+ */
+ attribute AString iconClass;
+
+ /**
+ * Textual id of the XBL binding that will be used to represent the
+ * activity in the Activity Manager window.
+ *
+ * This attribute allows to associate default activity components
+ * with custom XBL bindings. See |activity.xml| file for default
+ * activity XBL bindings, and |activity.css| file for default binding
+ * associations.
+ */
+ attribute AString bindingName;
+
+ /**
+ * Defines the grouping style of the activity when being shown in the
+ * activity manager window:
+ * GROUPING_STYLE_STANDALONE or GROUPING_STYLE_BYCONTEXT
+ */
+ attribute short groupingStyle;
+
+ /**
+ * A text value to associate a facet type with the activity. If empty,
+ * the activity will be shown in the 'Misc' section.
+ */
+ attribute AString facet;
+
+ // UI related attributes end.
+
+ /**
+ * Gets the initiator of the activity. An initiator represents an object
+ * that generates and controls the activity. For example, Copy Service can be
+ * the initiator of the copy, and move activities. Similarly Gloda can be the
+ * initiator of indexing activity, etc.
+ *
+ * This attribute is used mostly by handler components to change the execution
+ * flow of the activity such as canceling, pausing etc. Since not used by the
+ * Activity Manager, it is not mandatory to set it.
+ *
+ * An initiator can be any JS Object or an XPCOM component that provides an
+ * nsIVariant interface.
+ */
+ readonly attribute nsIVariant initiator;
+
+ /**
+ * Adds an object to the activity's internal subject list. Subject list
+ * provides argument(s) to activity handlers to complete their operation.
+ * For example, nsIActivityUndoHandler needs the source and destination
+ * folders to undo a move operation.
+ *
+ * Since subjects are not used by the Activity Manager, it is up to the
+ * activity developer to provide these objects.
+ *
+ * A subject can be any JS object or XPCOM component that supports nsIVariant
+ * interface.
+ */
+ void addSubject(in nsIVariant aSubject);
+
+ /**
+ * Retrieves all subjects associated with this activity.
+ *
+ * @return The list of subject objects associated by the activity.
+ */
+ Array<nsIVariant> getSubjects();
+
+ /*
+ * Background:
+ * A context is a generic concept that is used to group the processes and
+ * warnings having similar properties such as same imap server, same smtp
+ * server etc.
+ * A context is uniquely identified by its "type" and "object" attributes.
+ * Each activity that has the same context type and object are considered
+ * belong to the same logical group, context.
+ *
+ * There are 4 predefined context types known by the Activity Manager:
+ * Account, Smtp, Calendar, and Addressbook. The most common context type
+ * for activities is the "Account Context" and when combined with an account
+ * server instance, it allows to group different activities happening on the
+ * the same account server.
+ */
+
+ /**
+ * Sets and gets the context object of the activity. A context object can be
+ * any JS object or XPCOM component that supports nsIVariant interface.
+ */
+ attribute nsIVariant contextObj;
+
+ /**
+ * Sets and gets the context type of the activity. If this is set, then
+ * the contextDisplayText should also be set.
+ */
+ attribute AString contextType;
+
+ /**
+ * Return the displayText to be used for the context
+ **/
+ attribute AString contextDisplayText;
+
+ /**
+ * Adds a listener. See nsIActivityListener below.
+ */
+ void addListener(in nsIActivityListener aListener);
+
+ /**
+ * Removes the given listener. See nsIActivityListener below.
+ */
+ void removeListener(in nsIActivityListener aListener);
+};
+
+
+/**
+ * A Process represents an on-going activity.
+ */
+[scriptable, uuid(9DC7CA67-828D-4AFD-A5C6-3ECE091A98B8)]
+interface nsIActivityProcess : nsIActivity {
+
+ /**
+ * Default state for uninitialized process activity
+ * object.
+ */
+ const short STATE_NOTSTARTED = -1;
+
+ /**
+ * Activity is currently in progress.
+ */
+ const short STATE_INPROGRESS = 0;
+
+ /**
+ * Activity is completed.
+ */
+ const short STATE_COMPLETED = 1;
+
+ /**
+ * Activity was canceled by the user.
+ * (same as completed)
+ */
+ const short STATE_CANCELED = 2;
+
+ /**
+ * Activity was paused by the user.
+ */
+ const short STATE_PAUSED = 3;
+
+ /**
+ * Activity waits for the user input's to retry.
+ * (i.e. login password)
+ */
+ const short STATE_WAITINGFORINPUT = 4;
+
+ /**
+ * Activity is ready for an automatic or manual retry.
+ */
+ const short STATE_WAITINGFORRETRY = 5;
+
+ /**
+ * The state of the activity.
+ * See above for possible values.
+ * @exception NS_ERROR_ILLEGAL_VALUE if the state isn't one of the states
+ * defined above.
+ */
+ attribute short state;
+
+ /**
+ * The percentage of activity completed.
+ * If the max value is unknown it'll be -1 here.
+ */
+ readonly attribute long percentComplete;
+
+ /**
+ * A brief text about the process' status.
+ */
+ readonly attribute AString lastStatusText;
+
+ /**
+ * The amount of work units completed so far.
+ */
+ readonly attribute unsigned long workUnitComplete;
+
+ /**
+ * Total amount of work units.
+ */
+ readonly attribute unsigned long totalWorkUnits;
+
+ /**
+ * The starting time of the process.
+ * 64-bit signed integers relative to midnight (00:00:00), January 1, 1970
+ * Greenwich Mean Time (GMT). (GMT is also known as Coordinated Universal
+ * Time, UTC.). The units of time are in microseconds.
+ *
+ * In JS Date.now(), in C++ PR_Now() / PR_USEC_PER_MSEC can be used to set
+ * this value.
+ */
+ readonly attribute long long startTime;
+
+ /**
+ * The handler to invoke when the cancel button is pressed. If present
+ * (non-null), the activity can be canceled and a cancel button will be
+ * displayed to the user. If null, it cannot be canceled and no button will
+ * be displayed.
+ */
+ attribute nsIActivityCancelHandler cancelHandler;
+
+ /**
+ * The handler to invoke when the pause button is pressed. If present
+ * (non-null), the activity can be pauseable and a pause button will be
+ * displayed to the user. If null, it cannot be paused and no button will
+ * be displayed.
+ */
+ attribute nsIActivityPauseHandler pauseHandler;
+
+ /**
+ * The handler to invoke when the retry button is pressed. If present
+ * (non-null), the activity can be retryable and a retry button will be
+ * displayed to the user. If null, it cannot be retried and no button will
+ * be displayed.
+ */
+ attribute nsIActivityRetryHandler retryHandler;
+
+ /**
+ * Updates the activity progress info.
+ *
+ * @param aStatusText A localized text describing the current status of the
+ * process
+ * @param aWorkUnitComplete The amount of work units completed. Not used by
+ * Activity Manager or default binding for any
+ * purpose.
+ * @param aTotalWorkUnits Total amount of work units. Not used by
+ * Activity Manager or default binding for any
+ * purpose. If set to zero, this indicates that the
+ * number of work units is unknown, and the percentage
+ * attribute will be set to -1.
+ */
+ void setProgress(in AString aStatusText,
+ in unsigned long aWorkUnitComplete,
+ in unsigned long aTotalWorkUnits);
+
+ /**
+ * Component initialization method.
+ *
+ * @param aDisplayText A localized text to be shown on the Activity Manager
+ * window
+ * @param aInitiator The initiator of the process
+ */
+ void init(in AString aDisplayText, in nsIVariant aInitiator);
+};
+
+/**
+ * Historical actions performed by the user, by extensions or by the system.
+ */
+[scriptable, uuid(5B1B0D03-2820-4E37-8BF8-102AFDE4FC45)]
+interface nsIActivityEvent : nsIActivity {
+
+ /**
+ * Any localized textual information related to this event.
+ * It is shown at the bottom of the displayText area.
+ */
+ readonly attribute AString statusText;
+
+ /**
+ * The starting time of the event.
+ * 64-bit signed integers relative to midnight (00:00:00), January 1, 1970
+ * Greenwich Mean Time (GMT). (GMT is also known as Coordinated Universal
+ * Time, UTC.). The units of time are in microseconds.
+ *
+ * In JS Date.now(), in C++ PR_Now() / PR_USEC_PER_MSEC can be used to set
+ * this value.
+ */
+ readonly attribute long long startTime;
+
+ /**
+ * The completion time of the event in microseconds.
+ * 64-bit signed integers relative to midnight (00:00:00), January 1, 1970
+ * Greenwich Mean Time (GMT). (GMT is also known as Coordinated Universal
+ * Time, UTC.). The units of time are in microseconds.
+ *
+ * In JS Date.now(), in C++ PR_Now() / PR_USEC_PER_MSEC can be used to set
+ * this value.
+ */
+ readonly attribute long long completionTime;
+
+ /**
+ * The handler to invoke when the undo button is pressed. If present
+ * (non-null), the activity can be undoable and an undo button will be
+ * displayed to the user. If null, it cannot be undone and no button will
+ * be displayed.
+ */
+ attribute nsIActivityUndoHandler undoHandler;
+
+ /**
+ * Component initialization method.
+ *
+ * @param aDisplayText Any localized text describing the event and its context
+ * @param aInitiator The initiator of the event
+ * @param aStatusText Any localized additional information about the event
+ * @param aStartTime The starting time of the event
+ * @param aCompletionTime The completion time of the event
+ */
+ void init(in AString aDisplayText, in nsIVariant aInitiator,
+ in AString aStatusText, in long long aStartTime,
+ in long long aCompletionTime);
+};
+
+[scriptable, uuid(8265833e-c604-4585-a43c-a76bd8ed3a8c)]
+interface nsIActivityWarning : nsIActivity {
+
+ /**
+ * Any localized textual information related to this warning.
+ */
+ readonly attribute AString warningText;
+
+ /**
+ * The time of the warning.
+ * 64-bit signed integers relative to midnight (00:00:00), January 1, 1970
+ * Greenwich Mean Time (GMT). (GMT is also known as Coordinated Universal
+ * Time, UTC.). The units of time are in microseconds.
+ *
+ * In JS Date.now(), in C++ PR_Now() / PR_USEC_PER_MSEC can be used to set
+ * this value.
+ */
+ readonly attribute long long time;
+
+ /**
+ * Recovery tip of the warning, localized.
+ */
+ readonly attribute AString recoveryTipText;
+
+ /**
+ * The handler to invoke when the recover button is pressed. If present
+ * (non-null), the activity can be recoverable and a recover button will be
+ * displayed to the user. If null, it cannot be recovered and no button will
+ * be displayed.
+ */
+ attribute nsIActivityRecoveryHandler recoveryHandler;
+
+ /**
+ * Component initialization method.
+ *
+ * @param aWarningText The localized text that will be shown on the display
+ * area
+ * @param aInitiator The initiator of the warning
+ * @param aRecoveryTip A localized textual information to guide the user in
+ * order to recover from the warning situation.
+ */
+ void init(in AString aWarningText, in nsIVariant aInitiator,
+ in AString aRecoveryTip);
+};
+
+[scriptable, uuid(bd11519f-b297-4b34-a793-1861dc90d5e9)]
+interface nsIActivityListener : nsISupports {
+ /**
+ * Triggered after activity state is changed.
+ */
+ void onStateChanged(in nsIActivity aActivity, in short aOldState);
+
+ /**
+ * Triggered after the progress of the process activity is changed.
+ */
+ void onProgressChanged(in nsIActivity aActivity,
+ in AString aStatusText,
+ in unsigned long aWorkUnitsCompleted,
+ in unsigned long aTotalWorkUnits);
+
+ /**
+ * Triggered after one of the activity handler is set.
+ *
+ * This is mostly used to update the UI of the activity when
+ * one of the handler is set to null after the operation is completed.
+ * For example after the activity is undone, to make the undo button
+ * invisible.
+ */
+ void onHandlerChanged(in nsIActivity aActivity);
+};
diff --git a/comm/mail/components/activity/nsIActivityManager.idl b/comm/mail/components/activity/nsIActivityManager.idl
new file mode 100644
index 0000000000..860b4e1e2b
--- /dev/null
+++ b/comm/mail/components/activity/nsIActivityManager.idl
@@ -0,0 +1,135 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface mozIStorageConnection;
+interface nsIActivity;
+interface nsIActivityProcess;
+interface nsIVariant;
+
+/**
+ * See https://wiki.mozilla.org/Thunderbird:Activity_Manager/Developer for UML
+ * diagram and sample codes.
+ */
+
+/**
+ * An interface to get notified by the major Activity Manager events.
+ * Mostly used by UI glue code in activity.js.
+ */
+[scriptable, uuid(14cfad1c-3401-4c44-ab04-4a11b6662663)]
+interface nsIActivityMgrListener : nsISupports {
+ /**
+ * Called _after_ activity manager adds an activity into
+ * the managed list.
+ */
+ void onAddedActivity(in unsigned long aID, in nsIActivity aActivity);
+
+ /**
+ * Called _after_ activity manager removes an activity from
+ * the managed list.
+ */
+ void onRemovedActivity(in unsigned long aID);
+};
+
+/**
+ * Activity Manager is a simple component that understands how do display a
+ * combination of user activity and history. The activity manager works in
+ * conjunction with the 'Interactive Status Bar' to give the user the right
+ * level of notifications concerning what Thunderbird is doing on it's own and
+ * how Thunderbird has handled user requests.
+ *
+ * There are 3 different classifications of activity items which can be
+ * displayed in the Activity Manager Window:
+ * o Process: Processes are transient in the display. They are not written to
+ * disk as they are always acting on some data that already exists
+ * locally or remotely. If a process has finished and needs to keep
+ * some state for the user (like last sync time) it can convert
+ * itself into an event.
+ * o Event: Historical actions performed by the user and created by a process
+ * for the Activity Manager Window. Events can show up in the
+ * 'Interactive Status Bar' and be displayed to users as they are
+ * created.
+ * o Warning: Alerts sent by Thunderbird or servers (i.e. imap server) that need
+ * attention by the user. For example a Quota Alert from the imap
+ * server can be represented as a Warning to the user. They are not
+ * written to disk.
+ */
+[scriptable, uuid(9BFCC031-50E1-4D30-A35F-23509ABCB8D1)]
+interface nsIActivityManager : nsISupports {
+
+ /**
+ * Adds the given activity into the managed activities list.
+ *
+ * @param aActivity The activity that will be added.
+ *
+ * @return Unique activity identifier.
+ */
+ unsigned long addActivity(in nsIActivity aActivity);
+
+ /**
+ * Removes the activity with the given id if it's not currently
+ * in-progress.
+ *
+ * @param aID The unique ID of the activity.
+ *
+ * @throws NS_ERROR_FAILURE if the activity is in-progress.
+ */
+ void removeActivity(in unsigned long aID);
+
+ /**
+ * Retrieves an activity managed by the activity manager. This can be one that
+ * is in progress, or one that has completed in the past and is stored in the
+ * persistent store.
+ *
+ * @param aID The unique ID of the activity.
+ *
+ * @return The activity with the specified ID, or null if not found.
+ */
+ nsIActivity getActivity(in unsigned long aID);
+
+ /**
+ * Tests whether the activity in question in the activity list or not.
+ */
+ boolean containsActivity(in unsigned long aID);
+
+ /**
+ * Retrieves all activities managed by the activity manager. This can be one
+ * that is in progress (process), one that is represented as a warning, or one
+ * that has completed (event) in the past and is stored in the persistent
+ * store.
+ *
+ * @return A read-only list of activities managed by the activity manager.
+ */
+ Array<nsIActivity> getActivities();
+
+ /**
+ * Retrieves processes with given context type and object.
+ *
+ * @return A read-only list of processes matching to given criteria.
+ */
+ Array<nsIActivityProcess> getProcessesByContext(in AString aContextType,
+ in nsIVariant aContextObject);
+
+ /**
+ * Call to remove all activities apart from those that are in progress.
+ */
+ void cleanUp();
+
+ /**
+ * The number of processes in the activity list.
+ */
+ readonly attribute long processCount;
+
+ /**
+ * Adds a listener.
+ */
+ void addListener(in nsIActivityMgrListener aListener);
+
+ /**
+ * Removes the given listener.
+ */
+ void removeListener(in nsIActivityMgrListener aListener);
+};
diff --git a/comm/mail/components/activity/nsIActivityManagerUI.idl b/comm/mail/components/activity/nsIActivityManagerUI.idl
new file mode 100644
index 0000000000..07a2a30394
--- /dev/null
+++ b/comm/mail/components/activity/nsIActivityManagerUI.idl
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIInterfaceRequestor;
+
+[scriptable, uuid(ae7853b0-2e1f-4dc1-89cd-f8bbfb745d4d)]
+interface nsIActivityManagerUI : nsISupports {
+ /**
+ * The reason that should be passed when the user requests to show the
+ * activity manager's UI.
+ */
+ const short REASON_USER_INTERACTED = 0;
+
+ /**
+ * The reason that should be passed to the show method when we are displaying
+ * the UI because a new activity is being added to it.
+ */
+ const short REASON_NEW_ACTIVITY = 1;
+
+ /**
+ * Shows the Activity Manager's UI to the user.
+ *
+ * @param [optional] aWindowContext
+ * The parent window context to show the UI.
+ * @param [optional] aID
+ * The id of the activity to be preselected upon opening.
+ * @param [optional] aReason
+ * The reason to show the activity manager's UI. This defaults to
+ * REASON_USER_INTERACTED, and should be one of the previously listed
+ * constants.
+ */
+ void show([optional] in nsIInterfaceRequestor aWindowContext,
+ [optional] in unsigned long aID,
+ [optional] in short aReason);
+
+ /**
+ * Indicates if the UI is visible or not.
+ */
+ readonly attribute boolean visible;
+
+ /**
+ * Brings attention to the UI if it is already visible
+ *
+ * @throws NS_ERROR_UNEXPECTED if the UI is not visible.
+ */
+ void getAttention();
+};