Fix bug 776673 - Allow easily observing all calendars at once. r=mmecca
authorPhilipp Kewisch <mozilla@kewis.ch>
Thu, 09 Aug 2012 20:37:59 +0200
changeset 13230 71eae60afbf60a611153125ea034c2f92e9ed511
parent 13229 7ce307d297ff36486fbaaa0b3dc0d78fe13176eb
child 13231 2019e8845fbfd133c0d6dc246c1ae6392dc4cfa6
push idunknown
push userunknown
push dateunknown
reviewersmmecca
bugs776673
Fix bug 776673 - Allow easily observing all calendars at once. r=mmecca
calendar/base/modules/calUtils.jsm
calendar/base/public/calICalendar.idl
calendar/base/public/calICalendarManager.idl
calendar/base/src/calCalendarManager.js
calendar/base/src/calUtils.js
calendar/test/unit/test_calmgr.js
calendar/test/unit/xpcshell.ini
--- a/calendar/base/modules/calUtils.jsm
+++ b/calendar/base/modules/calUtils.jsm
@@ -1,16 +1,18 @@
 /* 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/. */
 
 // New code must not load/import calUtils.js, but should use calUtils.jsm.
 
 var gCalThreadingEnabled;
 
+Components.utils.import("resource:///modules/XPCOMUtils.jsm");
+
 EXPORTED_SYMBOLS = ["cal"];
 let cal = {
     // new code should land here,
     // and more code should be moved from calUtils.js into this object to avoid
     // clashes with other extensions
 
     getThreadManager: generateServiceAccessor("@mozilla.org/thread-manager;1",
                                               Components.interfaces.nsIThreadManager),
@@ -61,16 +63,66 @@ let cal = {
         if (this.threadingEnabled) {
             cal.getThreadManager().currentThread.dispatch({ run: func },
                                                           Components.interfaces.nsIEventTarget.DISPATCH_NORMAL);
         } else {
             func();
         }
     },
 
+    /**
+     * Create an adapter for the given interface. If passed, methods will be
+     * added to the template object, otherwise a new object will be returned.
+     *
+     * @param iface     The interface to adapt (Components.interfaces...)
+     * @param template  (optional) A template object to extend
+     * @return          If passed the adapted template object, otherwise a
+     *                    clean adapter.
+     *
+     * Currently supported interfaces are:
+     *  - calIObserver
+     *  - calICalendarManagerObserver
+     *  - calIOperationListener
+     *  - calICompositeObserver
+     */
+    createAdapter: function createAdapter(iface, template) {
+        let methods;
+        let adapter = template || {};
+        switch (iface) {
+            case Components.interfaces.calIObserver:
+                methods = ["onStartBatch", "onEndBatch", "onLoad", "onAddItem",
+                           "onModifyItem", "onDeleteItem", "onError",
+                           "onPropertyChanged", "onPropertyDeleting"];
+                break;
+            case Components.interfaces.calICalendarManagerObserver:
+                methods = ["onCalendarRegistered", "onCalendarUnregistering",
+                           "onCalendarDeleting"];
+                break;
+            case Components.interfaces.calIOperationListener:
+                methods = ["onGetResult", "onOperationComplete"];
+                break;
+            case Components.interfaces.calICompositeObserver:
+                methods = ["onCalendarAdded", "onCalendarRemoved",
+                           "onDefaultCalendarChanged"];
+                break;
+            default:
+                methods = [];
+                break;
+        }
+
+        for each (let method in methods) {
+            if (!(method in template)) {
+                adapter[method] = function() {};
+            }
+        }
+        adapter.QueryInterface = XPCOMUtils.generateQI([iface]);
+
+        return adapter;
+    },
+
     get threadingEnabled() {
         if (gCalThreadingEnabled === undefined) {
             gCalThreadingEnabled = !cal.getPrefSafe("calendar.threading.disabled", false);
         }
         return gCalThreadingEnabled;
     },
 
     /**
--- a/calendar/base/public/calICalendar.idl
+++ b/calendar/base/public/calICalendar.idl
@@ -528,16 +528,19 @@ interface calICompositeCalendar : calICa
    * Sets a statusobserver for status notifications like startMeteors() and StopMeteors().
    */
   void setStatusObserver(in calIStatusObserver aStatusObserver, in nsIDOMChromeWindow aWindow);
 };
 
 /**
  * Make a more general nsIObserverService2 and friends to support
  * nsISupports data and use that instead?
+ *
+ * NOTE: When adding methods here, please also add them in calUtils.jsm's
+ * createAdapter() method.
  */
 [scriptable, uuid(2953c9b2-2c73-11d9-80b6-00045ace3b8d)]
 interface calIObserver : nsISupports
 {
   void onStartBatch();
   void onEndBatch();
   void onLoad( in calICalendar aCalendar );
   void onAddItem( in calIItemBase aItem );
@@ -567,16 +570,19 @@ interface calICompositeObserver : calIOb
   void onCalendarRemoved( in calICalendar aCalendar );
   void onDefaultCalendarChanged( in calICalendar aNewDefaultCalendar );
 };
 
 /**
  * Async operations are called back via this interface.  If you know that your
  * object is not going to get called back for either of these methods, having
  * them return NS_ERROR_NOT_IMPLEMENTED is reasonable.
+ *
+ * NOTE: When adding methods here, please also add them in calUtils.jsm's
+ * createAdapter() method.
  */
 [scriptable, uuid(ed3d87d8-2c77-11d9-8f5f-00045ace3b8d)]
 interface calIOperationListener : nsISupports
 {
   /**
    * For add, modify, and delete.
    *
    * @param aCalendar       the calICalendar on which the operation took place
--- a/calendar/base/public/calICalendarManager.idl
+++ b/calendar/base/public/calICalendarManager.idl
@@ -1,21 +1,22 @@
 /* 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 calICalendar;
+interface calIObserver;
 interface nsIURI;
 interface nsIVariant;
 
 interface calICalendarManagerObserver;
 
-[scriptable, uuid(43a71d40-7807-4e2f-b741-2926ee73f89a)]
+[scriptable, uuid(613fb99f-359c-4a12-9c87-10b2b968f8cd)]
 interface calICalendarManager : nsISupports
 {
   /**
    * Gives the number of registered calendars that require network access.
    */
   readonly attribute PRUint32 networkCalendarCount;
 
   /***
@@ -44,36 +45,49 @@ interface calICalendarManager : nsISuppo
 
   /* get a calendar by its id */
   calICalendar getCalendarById(in AUTF8String aId);
 
   /* return a list of all calendars currently registered */
   void getCalendars(out PRUint32 count,
                     [array, size_is(count), retval] out calICalendar aCalendars);
 
+  /** Add an observer for the calendar manager, i.e when calendars are registered */
+  void addObserver(in calICalendarManagerObserver aObserver);
+  /** Remove an observer for the calendar manager */
+  void removeObserver(in calICalendarManagerObserver aObserver);
+
+  /** Add an observer to handle changes to all calendars (even disabled or unchecked ones) */
+  void addCalendarObserver(in calIObserver aObserver);
+  /** Remove an observer to handle changes to all calendars */
+  void removeCalendarObserver(in calIObserver aObserver);
 
   /* XXX private, don't use:
          will vanish as soon as providers will directly read/write from moz prefs
   */
   nsIVariant getCalendarPref_(in calICalendar aCalendar,
                               in AUTF8String aName);
   void setCalendarPref_(in calICalendar aCalendar,
                         in nsIVariant aName,
                         in nsIVariant aValue);
   void deleteCalendarPref_(in calICalendar aCalendar,
                            in AUTF8String aName);
   
-  void addObserver(in calICalendarManagerObserver aObserver);
-  void removeObserver(in calICalendarManagerObserver aObserver);
 };
 
+/**
+ * Observer to handle actions done by the calendar manager
+ *
+ * NOTE: When adding methods here, please also add them in calUtils.jsm's
+ * createAdapter() method.
+ */
 [scriptable, uuid(383f36f1-e669-4ca4-be7f-06b43910f44a)]
 interface calICalendarManagerObserver : nsISupports
 {
-  // called after the calendar is registered
+  /** Called after the calendar is registered */
   void onCalendarRegistered(in calICalendar aCalendar);
 
-  // called before the unregister actually takes place
+  /** Called before the unregister actually takes place */
   void onCalendarUnregistering(in calICalendar aCalendar);
 
-  // called before the delete actually takes place
+  /** Called before the delete actually takes place */
   void onCalendarDeleting(in calICalendar aCalendar);
 };
--- a/calendar/base/src/calCalendarManager.js
+++ b/calendar/base/src/calCalendarManager.js
@@ -5,123 +5,41 @@
 Components.utils.import("resource://gre/modules/AddonManager.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 Components.utils.import("resource://calendar/modules/calUtils.jsm");
 Components.utils.import("resource://calendar/modules/calProviderUtils.jsm");
 
 const REGISTRY_BRANCH = "calendar.registry.";
 const DB_SCHEMA_VERSION = 10;
 
-function getPrefBranchFor(id) {
-    return (REGISTRY_BRANCH + id + ".");
-}
-
-/**
- * Helper function to flush the preferences file. If the application crashes
- * after a calendar has been created using the prefs registry, then the calendar
- * won't show up. Writing the prefs helps counteract.
- */
-function flushPrefs() {
-    Components.classes["@mozilla.org/preferences-service;1"]
-              .getService(Components.interfaces.nsIPrefService)
-              .savePrefFile(null);
-}
-
-
-/**
- * Callback object for the refresh timer. Should be called as an object, i.e
- * let foo = new timerCallback(calendar);
- *
- * @param aCalendar     The calendar to refresh on notification
- */
-function timerCallback(aCalendar) {
-    this.notify = function refreshNotify(aTimer) {
-        if (!aCalendar.getProperty("disabled") && aCalendar.canRefresh) {
-            aCalendar.refresh();
-        }
-    }
-}
-
-var gCalendarManagerAddonListener = {
-    onDisabling: function(aAddon, aNeedsRestart) {
-        if (!this.queryUninstallProvider(aAddon)) {
-            // If the addon should not be disabled, then re-enable it.
-            aAddon.userDisabled = false;
-        }
-    },
-
-    onUninstalling: function(aAddon, aNeedsRestart) {
-        if (!this.queryUninstallProvider(aAddon)) {
-            // If the addon should not be uninstalled, then cancel the uninstall.
-            aAddon.cancelUninstall();
-        }
-    },
-
-    queryUninstallProvider: function(aAddon) {
-        const uri = "chrome://calendar/content/calendar-providerUninstall-dialog.xul";
-        const features = "chrome,titlebar,resizable,modal";
-        let calMgr = cal.getCalendarManager();
-        let affectedCalendars =
-            [ calendar for each (calendar in calMgr.getCalendars({}))
-              if (calendar.providerID == aAddon.id) ];
-        if (!affectedCalendars.length) {
-            // If no calendars are affected, then everything is fine.
-            return true;
-        }
-
-        let args = { shouldUninstall: false, extension: aAddon };
-
-        // Now find a window. The best choice would be the most recent
-        // addons window, otherwise the most recent calendar window, or we
-        // create a new toplevel window.
-        let win = Services.wm.getMostRecentWindow("Extension:Manager") ||
-                  cal.getCalendarWindow();
-        if (win) {
-            win.openDialog(uri, "CalendarProviderUninstallDialog", features, args);
-        } else {
-            // Use the window watcher to open a parentless window.
-            Services.ww.openWindow(null, uri, "CalendarProviderUninstallWindow", features, args);
-        }
-
-        // Now that we are done, check if the dialog was accepted or canceled.
-        return args.shouldUninstall;
-    }
-};
-
 function calCalendarManager() {
     this.wrappedJSObject = this;
     this.mObservers = new calListenerBag(Components.interfaces.calICalendarManagerObserver);
+    this.mCalendarObservers = new calListenerBag(Components.interfaces.calIObserver);
 }
 
 calCalendarManager.prototype = {
-    contractID: "@mozilla.org/calendar/manager;1",
-    classDescription: "Calendar Manager",
     classID: Components.ID("{f42585e7-e736-4600-985d-9624c1c51992}"),
+    QueryInterface: XPCOMUtils.generateQI([
+        Components.interfaces.calICalendarManager,
+        Components.interfaces.calIStartupService,
+        Components.interfaces.nsIObserver,
+    ]),
 
-    getInterfaces: function (count) {
-        let ifaces = [
-            Components.interfaces.nsISupports,
+    classInfo: XPCOMUtils.generateCI({
+        classID: Components.ID("{f42585e7-e736-4600-985d-9624c1c51992}"),
+        contractID: "@mozilla.org/calendar/manager;1",
+        classDescription: "Calendar Manager",
+        interfaces: [
             Components.interfaces.calICalendarManager,
             Components.interfaces.calIStartupService,
             Components.interfaces.nsIObserver,
-            Components.interfaces.nsIClassInfo
-        ];
-        count.value = ifaces.length;
-        return ifaces;
-    },
-
-    getHelperForLanguage: function (language) {
-        return null;
-    },
-
-    implementationLanguage: Components.interfaces.nsIProgrammingLanguage.JAVASCRIPT,
-    flags: Components.interfaces.nsIClassInfo.SINGLETON,
-    QueryInterface: function (aIID) {
-        return cal.doQueryInterface(this, calCalendarManager.prototype, aIID, null, this);
-    },
+        ],
+        flags: Components.interfaces.nsIClassInfo.SINGLETON
+    }),
 
     get networkCalendarCount() {
         return this.mNetworkCalendarCount;
     },
 
     get readOnlyCalendarCount() {
         return this.mReadonlyCalendarCount;
     },
@@ -556,20 +474,16 @@ calCalendarManager.prototype = {
             AddonManager.getAddonByID("{e2fda1a4-762b-4020-b5ad-a41df1933103}", function getLightningExt(aAddon) {
                 aAddon.userDisabled = true;
                 startup.quit(Components.interfaces.nsIAppStartup.eRestart |
                     Components.interfaces.nsIAppStartup.eForceQuit);
             });
         }
     },
 
-    notifyObservers: function(functionName, args) {
-        this.mObservers.notify(functionName, args);
-    },
-
     /**
      * calICalendarManager interface
      */
     createCalendar: function cmgr_createCalendar(type, uri) {
         try {
             if (!Components.classes["@mozilla.org/calendar/calendar;1?type=" + type]) {
                 // Don't notify the user with an extra dialog if the provider
                 // interface is missing.
@@ -585,16 +499,17 @@ calCalendarManager.prototype = {
             if (ex instanceof Components.interfaces.nsIException) {
                 rc = ex.result;
                 uiMessage = ex.message;
             }
             switch (rc) {
                 case Components.interfaces.calIErrors.STORAGE_UNKNOWN_SCHEMA_ERROR:
                     // For now we alert and quit on schema errors like we've done before:
                     this.alertAndQuit();
+                    return;
                 case Components.interfaces.calIErrors.STORAGE_UNKNOWN_TIMEZONES_ERROR:
                     uiMessage = calGetString("calendar", "unknownTimezonesError", [uri.spec]);
                     break;
                 default:
                     uiMessage = calGetString("calendar", "unableToCreateProvider", [uri.spec]);
                     break;
             }
             // Log the original exception via error console to provide more debug info
@@ -770,31 +685,29 @@ calCalendarManager.prototype = {
         return calendars;
     },
 
     assureCache: function cmgr_assureCache() {
         if (!this.mCache) {
             this.mCache = {};
             this.mCalObservers = {};
 
-            let prefService = Components.classes["@mozilla.org/preferences-service;1"]
-                                        .getService(Components.interfaces.nsIPrefBranch);
             let allCals = {};
-            for each (let key in prefService.getChildList(REGISTRY_BRANCH)) { // merge down all keys
+            for each (let key in Services.prefs.getChildList(REGISTRY_BRANCH)) { // merge down all keys
                 allCals[key.substring(0, key.indexOf(".", REGISTRY_BRANCH.length))] = true;
             }
 
             for (let calBranch in allCals) {
                 let id = calBranch.substring(REGISTRY_BRANCH.length);
                 let ctype = cal.getPrefSafe(calBranch + ".type", null);
                 let curi = cal.getPrefSafe(calBranch + ".uri", null);
 
                 try {
                     if (!ctype || !curi) { // sanity check
-                        prefService.deleteBranch(calBranch + ".");
+                        Services.prefs.deleteBranch(calBranch + ".");
                         continue;
                     }
 
                     let uri = cal.makeURL(curi);
                     let calendar = this.createCalendar(ctype, uri);
                     if (calendar) {
                         calendar.id = id;
                         if (calendar.getProperty("auto-enabled")) {
@@ -847,47 +760,42 @@ calCalendarManager.prototype = {
     },
 
     setCalendarPref_: function(calendar, name, value) {
         cal.ASSERT(calendar, "Invalid Calendar!");
         cal.ASSERT(calendar.id !== null, "Calendar id needs to be set!");
         cal.ASSERT(name && name.length > 0, "Pref Name must be non-empty!");
 
         let branch = (getPrefBranchFor(calendar.id) + name);
-
-        let prefService = Components.classes["@mozilla.org/preferences-service;1"]
-                                    .getService(Components.interfaces.nsIPrefBranch);
-        // delete before to allow pref-type changes:
-        prefService.deleteBranch(branch);
+        // Delete before to allow pref-type changes:
+        Services.prefs.deleteBranch(branch);
 
         if ( name === "name" ) {
             cal.setLocalizedPref(branch, value);
         } else {
             cal.setPref(branch, value);
         }
     },
 
     deleteCalendarPref_: function(calendar, name) {
         cal.ASSERT(calendar, "Invalid Calendar!");
         cal.ASSERT(calendar.id !== null, "Calendar id needs to be set!");
         cal.ASSERT(name && name.length > 0, "Pref Name must be non-empty!");
-
-        let prefService = Components.classes["@mozilla.org/preferences-service;1"]
-                                    .getService(Components.interfaces.nsIPrefBranch);
-        prefService.deleteBranch(getPrefBranchFor(calendar.id) + name);
+        Services.prefs.deleteBranch(getPrefBranchFor(calendar.id) + name);
     },
 
     mObservers: null,
-    addObserver: function(aObserver) {
-        this.mObservers.add(aObserver);
-    },
+    addObserver: function(aObserver) this.mObservers.add(aObserver),
+    removeObserver: function(aObserver) this.mObservers.remove(aObserver),
+    notifyObservers: function(functionName, args) this.mObservers.notify(functionName, args),
 
-    removeObserver: function(aObserver) {
-        this.mObservers.remove(aObserver);
-    }
+    mCalendarObservers: null,
+    addCalendarObserver: function(aObserver) this.mCalendarObservers.add(aObserver),
+    removeCalendarObserver: function(aObserver) this.mCalendarObservers.remove(aObserver),
+    notifyCalendarObservers: function(functionName, args) this.mCalendarObservers.notify(functionName, args)
 };
 
 function equalMessage(msg1, msg2) {
     if (msg1.GetString(0) == msg2.GetString(0) &&
         msg1.GetString(1) == msg2.GetString(1) &&
         msg1.GetString(2) == msg2.GetString(2)) {
         return true;
     }
@@ -902,113 +810,114 @@ function calMgrCalendarObserver(calendar
     this.calMgr = calMgr;
 }
 
 calMgrCalendarObserver.prototype = {
     calendar: null,
     storedReadOnly: null,
     calMgr: null,
 
-    QueryInterface: function mBL_QueryInterface(aIID) {
-        return doQueryInterface(this, calMgrCalendarObserver.prototype, aIID,
-                                [Components.interfaces.nsIWindowMediatorListener,
-                                 Components.interfaces.calIObserver]);
-    },
+    QueryInterface: XPCOMUtils.generateQI([
+        Components.interfaces.nsIWindowMediatorListener,
+        Components.interfaces.calIObserver
+    ]),
 
     // calIObserver:
-    onStartBatch: function() {},
-    onEndBatch: function() {},
-    onLoad: function(calendar) {},
-    onAddItem: function(aItem) {},
-    onModifyItem: function(aNewItem, aOldItem) {},
-    onDeleteItem: function(aDeletedItem) {},
+    onStartBatch: function() this.calMgr.notifyCalendarObservers("onStartBatch", arguments),
+    onEndBatch: function() this.calMgr.notifyCalendarObservers("onEndBatch", arguments),
+    onLoad: function(calendar) this.calMgr.notifyCalendarObservers("onLoad", arguments),
+    onAddItem: function(aItem) this.calMgr.notifyCalendarObservers("onAddItem", arguments),
+    onModifyItem: function(aNewItem, aOldItem) this.calMgr.notifyCalendarObservers("onModifyItem", arguments),
+    onDeleteItem: function(aDeletedItem) this.calMgr.notifyCalendarObservers("onDeleteItem", arguments),
     onError: function(aCalendar, aErrNo, aMessage) {
+        this.calMgr.notifyCalendarObservers("onError", arguments);
         this.announceError(aCalendar, aErrNo, aMessage);
     },
 
     onPropertyChanged: function(aCalendar, aName, aValue, aOldValue) {
+        this.calMgr.notifyCalendarObservers("onPropertyChanged", arguments);
         switch (aName) {
             case "requiresNetwork":
                 this.calMgr.mNetworkCalendarCount += (aValue ? 1 : -1);
                 break;
             case "readOnly":
                 this.calMgr.mReadonlyCalendarCount += (aValue ? 1 : -1);
                 break;
             case "refreshInterval":
                 this.calMgr.setupRefreshTimer(aCalendar);
                 break;
             case "cache.enabled":
-                aOldValue = aOldValue || false;
-                aValue = aValue || false;
-
-                if (aOldValue != aValue) {
-
-                    // Try to find the current sort order
-                    let sortOrderPref = cal.getPrefSafe("calendar.list.sortOrder", "").split(" ");
-                    let initialSortOrderPos = null;
-                    for (let i = 0; i < sortOrderPref.length; ++i) {
-                        if (sortOrderPref[i] == aCalendar.id) {
-                            initialSortOrderPos = i;
-                        }
-                    }
-                    // Enabling or disabling cache on a calendar re-creates
-                    // it so the registerCalendar call can wrap/unwrap the
-                    // calCachedCalendar facade saving the user the need to
-                    // restart Thunderbird and making sure a new Id is used.
-                    this.calMgr.unregisterCalendar(aCalendar);
-                    this.calMgr.deleteCalendar(aCalendar);
-                    var newCal = this.calMgr.createCalendar(aCalendar.type,aCalendar.uri);
-                    newCal.name = aCalendar.name;
-
-                    // TODO: if properties get added this list will need to be adjusted,
-                    // ideally we should add a "getProperties" method to calICalendar.idl
-                    // to retrieve all non-transient properties for a calendar.
-                    let propsToCopy = [ "color",
-                                        "disabled",
-                                        "auto-enabled",
-                                        "cache.enabled",
-                                        "refreshInterval",
-                                        "suppressAlarms",
-                                        "calendar-main-in-composite",
-                                        "calendar-main-default",
-                                        "readOnly",
-                                        "imip.identity.key"];
-                    for each (let prop in propsToCopy ) {
-                      newCal.setProperty(prop,
-                                         aCalendar.getProperty(prop));
-                    }
-
-                    if (initialSortOrderPos != null) {
-                        newCal.setProperty("initialSortOrderPos",
-                                           initialSortOrderPos);
-                    }
-                    this.calMgr.registerCalendar(newCal);
-                }
-                else {
-                    if (aCalendar.wrappedJSObject instanceof calCachedCalendar) {
-                        // any attempt to switch this flag will reset the cached calendar;
-                        // could be useful for users in case the cache may be corrupted.
-                        aCalendar.wrappedJSObject.setupCachedCalendar();
-                    }
-                }
-                break;
+                this.changeCalendarCache.apply(this, arguments);
             case "disabled":
                 if (!aValue && aCalendar.canRefresh) {
                     aCalendar.refresh();
                 }
                 break;
         }
     },
 
+    changeCalendarCache: function(aCalendar, aName, aValue, aOldValue) {
+        aOldValue = aOldValue || false;
+        aValue = aValue || false;
+
+        if (aOldValue != aValue) {
+            // Try to find the current sort order
+            let sortOrderPref = cal.getPrefSafe("calendar.list.sortOrder", "").split(" ");
+            let initialSortOrderPos = null;
+            for (let i = 0; i < sortOrderPref.length; ++i) {
+                if (sortOrderPref[i] == aCalendar.id) {
+                    initialSortOrderPos = i;
+                }
+            }
+            // Enabling or disabling cache on a calendar re-creates
+            // it so the registerCalendar call can wrap/unwrap the
+            // calCachedCalendar facade saving the user the need to
+            // restart Thunderbird and making sure a new Id is used.
+            this.calMgr.unregisterCalendar(aCalendar);
+            this.calMgr.deleteCalendar(aCalendar);
+            var newCal = this.calMgr.createCalendar(aCalendar.type,aCalendar.uri);
+            newCal.name = aCalendar.name;
+
+            // TODO: if properties get added this list will need to be adjusted,
+            // ideally we should add a "getProperties" method to calICalendar.idl
+            // to retrieve all non-transient properties for a calendar.
+            let propsToCopy = [ "color",
+                                "disabled",
+                                "auto-enabled",
+                                "cache.enabled",
+                                "refreshInterval",
+                                "suppressAlarms",
+                                "calendar-main-in-composite",
+                                "calendar-main-default",
+                                "readOnly",
+                                "imip.identity.key"];
+            for each (let prop in propsToCopy ) {
+              newCal.setProperty(prop,
+                                 aCalendar.getProperty(prop));
+            }
+
+            if (initialSortOrderPos != null) {
+                newCal.setProperty("initialSortOrderPos",
+                                   initialSortOrderPos);
+            }
+            this.calMgr.registerCalendar(newCal);
+        } else {
+            if (aCalendar.wrappedJSObject instanceof calCachedCalendar) {
+                // any attempt to switch this flag will reset the cached calendar;
+                // could be useful for users in case the cache may be corrupted.
+                aCalendar.wrappedJSObject.setupCachedCalendar();
+            }
+        }
+    },
+
     onPropertyDeleting: function(aCalendar, aName) {
         this.onPropertyChanged(aCalendar, aName, false, true);
     },
 
     // Error announcer specific functions
-
     announceError: function(aCalendar, aErrNo, aMessage) {
 
         var paramBlock = Components.classes["@mozilla.org/embedcomp/dialogparam;1"]
                                    .createInstance(Components.interfaces.nsIDialogParamBlock);
         var sbs = Components.classes["@mozilla.org/intl/stringbundle;1"]
                             .getService(Components.interfaces.nsIStringBundleService);
         var props = sbs.createBundle("chrome://calendar/locale/calendar.properties");
         var errMsg;
@@ -1136,8 +1045,83 @@ calDummyCalendar.prototype = {
         switch (aName) {
             case "force-disabled":
                 return true;
             default:
                 return this.__proto__.__proto__.getProperty.apply(this, arguments);
         }
     }
 };
+
+function getPrefBranchFor(id) {
+    return (REGISTRY_BRANCH + id + ".");
+}
+
+/**
+ * Helper function to flush the preferences file. If the application crashes
+ * after a calendar has been created using the prefs registry, then the calendar
+ * won't show up. Writing the prefs helps counteract.
+ */
+function flushPrefs() {
+    Components.classes["@mozilla.org/preferences-service;1"]
+              .getService(Components.interfaces.nsIPrefService)
+              .savePrefFile(null);
+}
+
+/**
+ * Callback object for the refresh timer. Should be called as an object, i.e
+ * let foo = new timerCallback(calendar);
+ *
+ * @param aCalendar     The calendar to refresh on notification
+ */
+function timerCallback(aCalendar) {
+    this.notify = function refreshNotify(aTimer) {
+        if (!aCalendar.getProperty("disabled") && aCalendar.canRefresh) {
+            aCalendar.refresh();
+        }
+    }
+}
+
+var gCalendarManagerAddonListener = {
+    onDisabling: function(aAddon, aNeedsRestart) {
+        if (!this.queryUninstallProvider(aAddon)) {
+            // If the addon should not be disabled, then re-enable it.
+            aAddon.userDisabled = false;
+        }
+    },
+
+    onUninstalling: function(aAddon, aNeedsRestart) {
+        if (!this.queryUninstallProvider(aAddon)) {
+            // If the addon should not be uninstalled, then cancel the uninstall.
+            aAddon.cancelUninstall();
+        }
+    },
+
+    queryUninstallProvider: function(aAddon) {
+        const uri = "chrome://calendar/content/calendar-providerUninstall-dialog.xul";
+        const features = "chrome,titlebar,resizable,modal";
+        let calMgr = cal.getCalendarManager();
+        let affectedCalendars =
+            [ calendar for each (calendar in calMgr.getCalendars({}))
+              if (calendar.providerID == aAddon.id) ];
+        if (!affectedCalendars.length) {
+            // If no calendars are affected, then everything is fine.
+            return true;
+        }
+
+        let args = { shouldUninstall: false, extension: aAddon };
+
+        // Now find a window. The best choice would be the most recent
+        // addons window, otherwise the most recent calendar window, or we
+        // create a new toplevel window.
+        let win = Services.wm.getMostRecentWindow("Extension:Manager") ||
+                  cal.getCalendarWindow();
+        if (win) {
+            win.openDialog(uri, "CalendarProviderUninstallDialog", features, args);
+        } else {
+            // Use the window watcher to open a parentless window.
+            Services.ww.openWindow(null, uri, "CalendarProviderUninstallWindow", features, args);
+        }
+
+        // Now that we are done, check if the dialog was accepted or canceled.
+        return args.shouldUninstall;
+    }
+};
--- a/calendar/base/src/calUtils.js
+++ b/calendar/base/src/calUtils.js
@@ -1283,22 +1283,24 @@ function getProgressAtom(aTask) {
     }
 
     return "future";
 }
 
 /**
  * Returns true if we are Sunbird (according to our UUID), false otherwise.
  */
-function isSunbird()
-{
+function isSunbird() {
     if (isSunbird.mIsSunbird === undefined) {
-        var appInfo = Components.classes["@mozilla.org/xre/app-info;1"]
-                                .getService(Components.interfaces.nsIXULAppInfo);
-        isSunbird.mIsSunbird = (appInfo.ID == "{718e30fb-e89b-41dd-9da7-e25a45638b28}");
+        try {
+            isSunbird.mIsSunbird = (Services.appinfo.ID == "{718e30fb-e89b-41dd-9da7-e25a45638b28}");
+        } catch (e) {
+            dump("### Warning: Could not access appinfo, using unreliable check for Lightning\n");
+            isSunbird.mIsSunbird = !("@mozilla.org/lightning/mime-converter;1" in Components.classes);
+        }
     }
     return isSunbird.mIsSunbird;
 }
 
 function hasPositiveIntegerValue(elementId)
 {
     var value = document.getElementById(elementId).value;
     if (value && (parseInt(value) == value) && value > 0) {
new file mode 100644
--- /dev/null
+++ b/calendar/test/unit/test_calmgr.js
@@ -0,0 +1,192 @@
+/* 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/. */
+
+Components.utils.import("resource://calendar/modules/calUtils.jsm");
+Components.utils.import("resource:///modules/Services.jsm");
+
+/**
+ * Tests the calICalendarManager interface
+ */
+function run_test() {
+    do_get_profile();
+    add_test(test_registration);
+    add_test(test_calobserver);
+    cal.getCalendarManager().startup({ onResult: function() {
+        run_next_test();
+    }});
+}
+
+function test_calobserver() {
+    function checkCounters(add, modify, del, alladd, allmodify, alldel) {
+        do_check_eq(calcounter.addItem, add);
+        do_check_eq(calcounter.modifyItem, modify);
+        do_check_eq(calcounter.deleteItem, del);
+        do_check_eq(allcounter.addItem, alladd === undefined ? add : alladd);
+        do_check_eq(allcounter.modifyItem, allmodify === undefined ? modify : allmodify);
+        do_check_eq(allcounter.deleteItem, alldel === undefined ? del : alldel);
+        resetCounters();
+    }
+    function resetCounters() {
+        calcounter = { addItem: 0, modifyItem: 0, deleteItem: 0 };
+        allcounter = { addItem: 0, modifyItem: 0, deleteItem: 0 };
+    }
+
+    // First of all we need a local calendar to work on and some variables
+    let calmgr = cal.getCalendarManager();
+    let memory = calmgr.createCalendar("memory", Services.io.newURI("moz-memory-calendar://", null, null));
+    let memory2 = calmgr.createCalendar("memory", Services.io.newURI("moz-memory-calendar://", null, null));
+    let calcounter, allcounter;
+
+    // These observers will end up counting calls which we will use later on
+    let calobs = cal.createAdapter(Components.interfaces.calIObserver, {
+        onAddItem: function(itm) calcounter.addItem++,
+        onModifyItem: function(itm) calcounter.modifyItem++,
+        onDeleteItem: function(itm) calcounter.deleteItem++
+    });
+    let allobs = cal.createAdapter(Components.interfaces.calIObserver, {
+        onAddItem: function(itm) allcounter.addItem++,
+        onModifyItem: function(itm) allcounter.modifyItem++,
+        onDeleteItem: function(itm) allcounter.deleteItem++
+    });
+
+    // Set up counters and observers
+    resetCounters();
+    calmgr.registerCalendar(memory);
+    calmgr.registerCalendar(memory2);
+    calmgr.addCalendarObserver(allobs);
+    memory.addObserver(calobs);
+
+    // Add an item
+    let item = cal.createEvent();
+    item.id = cal.getUUID()
+    item.startDate = cal.now();
+    item.endDate = cal.now();
+    memory.addItem(item, null);
+    checkCounters(1, 0, 0);
+
+    // Modify the item
+    let newItem = item.clone();
+    newItem.title = "title";
+    memory.modifyItem(newItem, item, null);
+    checkCounters(0, 1, 0);
+
+    // Delete the item
+    newItem.generation++; // circumvent generation checks for easier code
+    memory.deleteItem(newItem, null);
+    checkCounters(0, 0, 1);
+
+    // Now check the same for adding the item to a calendar only observed by the
+    // calendar manager. The calcounters should still be 0, but the calendar
+    // manager counter should have an item added, modified and deleted
+    memory2.addItem(item, null);
+    memory2.modifyItem(newItem, item, null);
+    memory2.deleteItem(newItem, null);
+    checkCounters(0, 0, 0, 1, 1, 1);
+
+    // Remove observers
+    memory.removeObserver(calobs);
+    calmgr.removeCalendarObserver(allobs);
+
+    // Make sure removing it actually worked
+    memory.addItem(item, null);
+    memory.modifyItem(newItem, item, null);
+    memory.deleteItem(newItem, null);
+    checkCounters(0, 0, 0);
+
+    // We are done now, start the next test
+    run_next_test();
+}
+
+function test_registration() {
+    function checkCalendarCount(net, rdonly, all) {
+        do_check_eq(calmgr.networkCalendarCount, net);
+        do_check_eq(calmgr.readOnlyCalendarCount , rdonly);
+        do_check_eq(calmgr.calendarCount, all);
+    }
+    function checkRegistration(reg, unreg, del) {
+        do_check_eq(registered, reg);
+        do_check_eq(unregistered, unreg);
+        do_check_eq(deleted, del);
+        registered = false;
+        unregistered = false;
+        deleted = false;
+    }
+
+    // Initially there should be no calendars
+    let calmgr = cal.getCalendarManager();
+    checkCalendarCount(0, 0, 0);
+
+    // Create a local memory calendar, ths shouldn't register any calendars
+    let memory = calmgr.createCalendar("memory", Services.io.newURI("moz-memory-calendar://", null, null));
+    checkCalendarCount(0, 0, 0);
+
+    // Register an observer to test it.
+    let registered = false, unregistered = false, deleted = false, readOnly = false;
+    let mgrobs = cal.createAdapter(Components.interfaces.calICalendarManagerObserver, {
+        onCalendarRegistered: function onCalendarRegistered(aCalendar) {
+            if (aCalendar.id == memory.id) registered = true;
+        },
+        onCalendarUnregistering: function onCalendarUnregistering(aCalendar) {
+            if (aCalendar.id == memory.id) unregistered = true;
+        },
+        onCalendarDeleting: function onCalendarDeleting(aCalendar) {
+            if (aCalendar.id == memory.id) deleted = true;
+        }
+    });
+    let calobs = cal.createAdapter(Components.interfaces.calIObserver, {
+        onPropertyChanged: function onPropertyChanging(aCalendar, aName, aValue, aOldValue) {
+            do_check_eq(aCalendar.id, memory.id);
+            do_check_eq(aName, "readOnly");
+            readOnly = aValue;
+        }
+    });
+    memory.addObserver(calobs);
+    calmgr.addObserver(mgrobs);
+
+    // Register the calendar and check if its counted and observed
+    calmgr.registerCalendar(memory);
+    checkRegistration(true, false, false);
+    checkCalendarCount(0, 0, 1);
+
+    // The calendar should now have an id
+    do_check_neq(memory.id, null);
+
+    // And be in the list of calendars
+    do_check_true(memory == calmgr.getCalendarById(memory.id));
+    do_check_true(calmgr.getCalendars({}).some(function(x) x.id == memory.id));
+
+    // Make it readonly and check if the observer caught it
+    memory.setProperty("readOnly", true);
+    do_check_eq(readOnly, true);
+
+    // Now unregister it
+    calmgr.unregisterCalendar(memory);
+    checkRegistration(false, true, false);
+    checkCalendarCount(0, 0, 0);
+
+    // The calendar shouldn't be in the list of ids
+    do_check_eq(calmgr.getCalendarById(memory.id), null);
+    do_check_true(calmgr.getCalendars({}).every(function(x) x.id != memory.id));
+
+    // And finally delete it
+    calmgr.deleteCalendar(memory);
+    checkRegistration(false, false, true);
+    checkCalendarCount(0, 0, 0);
+
+    // Now remove the observer again
+    calmgr.removeObserver(mgrobs);
+    memory.removeObserver(calobs);
+
+    // Check if removing it actually worked
+    calmgr.registerCalendar(memory);
+    calmgr.unregisterCalendar(memory);
+    calmgr.deleteCalendar(memory);
+    memory.setProperty("readOnly", false);
+    checkRegistration(false, false, false);
+    do_check_eq(readOnly, true);
+    checkCalendarCount(0, 0, 0);
+
+    // We are done now, start the next test
+    run_next_test();
+}
--- a/calendar/test/unit/xpcshell.ini
+++ b/calendar/test/unit/xpcshell.ini
@@ -18,12 +18,13 @@ tail =
 [test_datetime.js]
 [test_datetime_before_1970.js]
 [test_freebusy.js]
 [test_hashedarray.js]
 [test_ics.js]
 [test_providers.js]
 [test_recur.js]
 [test_relation.js]
+[test_calmgr.js]
 
 # Bug 680201 Skip this test because it doesn't work properly yet.
 [test_webcal.js]
 skip-if = true